#pragma warning disable SA1512,SA1124 // Single-line comments should not be followed by blank line using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using NugetForUnity.Configuration; using NugetForUnity.Helper; using NugetForUnity.Models; using UnityEditor; using UnityEngine; #region No ReShaper // ReSharper disable All // needed because 'JetBrains.Annotations.NotNull' and 'System.Diagnostics.CodeAnalysis.NotNull' collide if this file is compiled with a never version of Unity / C# using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; // ReSharper restore All #endregion #pragma warning restore SA1512,SA1124 // Single-line comments should not be followed by blank line namespace NugetForUnity.PackageSource { /// /// API client for NuGet API v3. /// [Serializable] internal sealed class NugetApiClientV3 : IDisposable { [NonSerialized] [NotNull] private readonly Uri apiIndexJsonUrl; [NonSerialized] [NotNull] private readonly HttpClient httpClient; [NonSerialized] [CanBeNull] private TaskCompletionSource initializationTaskCompletionSource; // Example: https://api.nuget.org/v3-flatcontainer/ [CanBeNull] [SerializeField] private string packageBaseAddress; // Example: https://api.nuget.org/v3-flatcontainer/{0}/{1}/{0}.{1}.nupkg [CanBeNull] [SerializeField] private string packageDownloadUrlTemplate; // Example: https://api.nuget.org/v3/registration5-gz-semver2/ [CanBeNull] [SerializeField] private string registrationsBaseUrl; [CanBeNull] [SerializeField] private List searchQueryServices; /// /// Initializes a new instance of the class. /// /// The absolute 'index.json' URL of the API. public NugetApiClientV3([NotNull] string url) { if (string.IsNullOrWhiteSpace(url)) { throw new ArgumentException($"'{nameof(url)}' cannot be null or whitespace.", nameof(url)); } apiIndexJsonUrl = new Uri(url); var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; if (Application.platform == RuntimePlatform.WindowsEditor) { // On Windows, Mono HttpClient does not automatically pick up proxy settings. handler.Proxy = WebRequest.GetSystemWebProxy(); } httpClient = new HttpClient(handler); InitializeFromSessionState(); } /// /// Gets or sets a optional overwrite for the URL used to download '.nupkg' files (see: ). /// [CanBeNull] [field: SerializeField] public string PackageDownloadUrlTemplateOverwrite { get; set; } /// public void Dispose() { httpClient.Dispose(); } /// /// Searches for NuGet packages available on the server. /// /// /// https://github.com/NuGet/docs.microsoft.com-nuget/blob/live/docs/api/search-query-service-resource.md. /// /// The package source that owns this client. /// /// The search query. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters. /// /// /// The number of results to skip. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters. /// /// /// The number of results to return. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters. /// /// /// Whether to include pre-release packages. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters. /// /// /// Token to cancel the HTTP request. /// /// /// A list of s that match the search query. /// public async Task> SearchPackageAsync( NugetPackageSourceV3 packageSource, string searchQuery = "", int skip = -1, int take = -1, bool includePreRelease = false, CancellationToken cancellationToken = default) { var successfullyInitialized = await EnsureInitializedAsync(packageSource); if (!successfullyInitialized || searchQueryServices == null) { return new List(); } if (searchQueryServices.Count == 0) { Debug.LogError($"There are no {nameof(searchQueryServices)} specified in the API '{apiIndexJsonUrl}' so we can't search."); return new List(); } var queryBuilder = new QueryBuilder(); // so both SemVer 1.0.0 and SemVer 2.0.0 compatible packages are returned queryBuilder.Add("semVerLevel", "2.0.0"); queryBuilder.Add("q", searchQuery); if (skip > 0) { queryBuilder.Add("skip", skip.ToString(CultureInfo.InvariantCulture)); } if (take > 0) { queryBuilder.Add("take", take.ToString(CultureInfo.InvariantCulture)); } if (includePreRelease) { queryBuilder.Add("prerelease", "true"); } var query = queryBuilder.ToString(); var queryService = searchQueryServices[0]; var responseString = await GetStringFromServerAsync(packageSource, queryService + query, cancellationToken).ConfigureAwait(false); var searchResult = JsonUtility.FromJson(responseString); var results = searchResult.data ?? throw new InvalidOperationException($"missing 'data' property in search response:\n{responseString}"); return SearchResultToNugetPackages(results, packageSource); } /// /// Download the .nupkg file and store it inside a file at . /// /// The package source that owns this client. /// The package to download its .nupkg from. /// Path where the downloaded file is placed. /// Hint for the url used to download the .nupkg file from. /// The async task. [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "We intentionally use lower case.")] public async Task DownloadNupkgToFileAsync( NugetPackageSourceV3 packageSource, INugetPackageIdentifier package, string outputFilePath, string downloadUrlHint) { string downloadUrl; if (!string.IsNullOrEmpty(downloadUrlHint)) { downloadUrl = downloadUrlHint; } else { var successfullyInitialized = await EnsureInitializedAsync(packageSource); if (!successfullyInitialized) { return; } if (string.IsNullOrEmpty(packageDownloadUrlTemplate)) { Debug.LogError( $"There are no {nameof(packageBaseAddress)} specified in the API '{apiIndexJsonUrl}' so we can't download packages."); return; } var version = package.Version.ToLowerInvariant(); var id = package.Id.ToLowerInvariant(); downloadUrl = string.Format(packageDownloadUrlTemplate, id, version); } using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl)) { AddHeadersToRequest(request, packageSource, false); using (var response = await httpClient.SendAsync(request).ConfigureAwait(false)) { await EnsureResponseIsSuccessAsync(response).ConfigureAwait(false); using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { using (var fileStream = File.Create(outputFilePath)) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } } } } /// /// Fetches a single NuGet package from the package registration. Other than this also fetches a list of /// all available versions. /// /// The package source that owns this client. /// The package identifier to receive including the details. /// Token to cancel the HTTP request. /// The package or null if we didn't find it. public async Task GetPackageWithAllVersionsAsync( NugetPackageSourceV3 packageSource, INugetPackageIdentifier package, CancellationToken cancellationToken = default) { var registrationItems = await GetRegistrationPageItemsAsync(packageSource, package, cancellationToken); if (registrationItems is null) { return null; } var versions = new List(); RegistrationLeafObject latestVersionItem = null; NugetPackageVersion latestVersion = null; foreach (var item in registrationItems) { if (item.items is null || item.items.Count == 0) { item.items = await GetRegistrationPageLeafItems(packageSource, item, cancellationToken).ConfigureAwait(false); } foreach (var leafObject in item.items) { var catalogEntry = leafObject.CatalogEntry; if (catalogEntry.version is null) { throw new InvalidOperationException( $"missing '{nameof(catalogEntry.version)}' property in catalog entry:\n{JsonUtility.ToJson(catalogEntry)}"); } var version = new NugetPackageVersion(catalogEntry.version); versions.Add(version); if (latestVersion != null && version <= latestVersion) { continue; } latestVersion = version; latestVersionItem = leafObject; } } versions.Sort((v1, v2) => v2.CompareTo(v1)); return CreatePackageFromRegistrationLeaf(packageSource, latestVersionItem, versions); } /// /// Gets a single NuGet package including the package details but not containing all available versions. /// /// The package source that owns this client. /// The package identifier to receive including the details. /// Token to cancel the HTTP request. /// The package or null if we didn't find it. [ItemCanBeNull] public async Task GetPackageWithDetailsAsync( NugetPackageSourceV3 packageSource, INugetPackageIdentifier package, CancellationToken cancellationToken = default) { var leafItem = await GetPackageRegistrationLeafAsync(packageSource, package, cancellationToken).ConfigureAwait(false); if (leafItem is null) { return null; } return CreatePackageFromRegistrationLeaf(packageSource, leafItem); } /// /// Fetches the package details from the server. /// /// The package source that owns this client. /// The package to receive the details for. /// /// Token to cancel the request. /// /// /// The package details or null if the package is not found. /// [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "API uses lower case.")] [NotNull] [ItemNotNull] public async Task> GetPackageDetailsAsync( NugetPackageSourceV3 packageSource, INugetPackageIdentifier package, CancellationToken cancellationToken = default) { var leafItem = await GetPackageRegistrationLeafAsync(packageSource, package, cancellationToken).ConfigureAwait(false); return leafItem is null ? new List() : ConvertDependencyGroups(leafItem.CatalogEntry); } private static NugetPackageV3 CreatePackageFromRegistrationLeaf( NugetPackageSourceV3 packageSource, RegistrationLeafObject leafItem, List allVersions = null) { var entry = leafItem.CatalogEntry; if (entry.id is null) { throw new InvalidOperationException($"missing '{nameof(entry.id)}' property in catalog entry:\n{JsonUtility.ToJson(entry)}"); } if (entry.version is null) { throw new InvalidOperationException($"missing '{nameof(entry.version)}' property in catalog entry:\n{JsonUtility.ToJson(entry)}"); } return new NugetPackageV3( entry.id, entry.version, new List { entry.authors }, entry.description, 0, entry.licenseUrl, packageSource, entry.projectUrl, entry.summary, entry.title, entry.iconUrl, allVersions ?? new List { new NugetPackageVersion(entry.version) }) { DownloadUrl = leafItem.packageContent, Dependencies = ConvertDependencyGroups(entry), }; } private static List ConvertDependencyGroups(CatalogEntry entry) { if (entry.dependencyGroups != null) { return entry.dependencyGroups.ConvertAll( dependencyGroup => { var dependencies = dependencyGroup.dependencies is null ? new List() : dependencyGroup.dependencies.ConvertAll( dependency => (INugetPackageIdentifier)new NugetPackageIdentifier( dependency.id ?? throw new InvalidOperationException( $"missing '{nameof(dependency.id)}' inside '{nameof(dependencyGroup.dependencies)}' for dependency group: '{JsonUtility.ToJson(dependencyGroup)}'"), dependency.range)); return new NugetFrameworkGroup { Dependencies = dependencies, TargetFramework = dependencyGroup.targetFramework }; }); } if (ConfigurationManager.IsVerboseLoggingEnabled) { NugetLogger.LogVerbose( "missing '{0}.{1}' property for CatalogEntry: '{2}'", nameof(CatalogEntry), nameof(CatalogEntry.dependencyGroups), JsonUtility.ToJson(entry)); } return new List(); } private static List SearchResultToNugetPackages(List searchResults, NugetPackageSourceV3 packageSource) { var packages = new List(searchResults.Count); foreach (var item in searchResults) { if (item.versions is null) { throw new InvalidOperationException( $"missing '{nameof(item.versions)}' property in search result item:\n{JsonUtility.ToJson(item)}"); } if (item.id is null) { throw new InvalidOperationException($"missing '{nameof(item.id)}' property in search result item:\n{JsonUtility.ToJson(item)}"); } if (item.version is null) { throw new InvalidOperationException( $"missing '{nameof(item.version)}' property in search result item:\n{JsonUtility.ToJson(item)}"); } var versions = item.versions.ConvertAll(searchVersion => new NugetPackageVersion(searchVersion.version)); versions.Sort((v1, v2) => v2.CompareTo(v1)); packages.Add( new NugetPackageV3( item.id, item.version, item.authors ?? new List(), item.description, item.totalDownloads, item.licenseUrl, packageSource, item.projectUrl, item.summary, item.title, item.iconUrl, versions)); } return packages; } private static async Task EnsureResponseIsSuccessAsync(HttpResponseMessage response) { if (response.IsSuccessStatusCode) { return; } var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); throw new HttpRequestException( $"The request to '{response.RequestMessage.RequestUri}' failed with status code '{response.StatusCode}' and message: {responseString}"); } [ItemCanBeNull] private async Task GetPackageRegistrationLeafAsync( NugetPackageSourceV3 packageSource, INugetPackageIdentifier package, CancellationToken cancellationToken = default) { var registrationItems = await GetRegistrationPageItemsAsync(packageSource, package, cancellationToken); if (registrationItems is null) { return null; } var getLatestVersion = string.IsNullOrEmpty(package.Version); var pageItem = getLatestVersion ? registrationItems.OrderByDescending(registrationItem => new NugetPackageVersion(registrationItem.lower)).First() : registrationItems.Find( registrationItem => new NugetPackageVersion($"[{registrationItem.lower},{registrationItem.upper}]").InRange(package.PackageVersion)); if (pageItem is null) { Debug.LogError($"There is no package with id '{package.Id}' and version '{package.Version}' on the registration page."); return null; } if (pageItem.items is null || pageItem.items.Count == 0) { pageItem.items = await GetRegistrationPageLeafItems(packageSource, pageItem, cancellationToken).ConfigureAwait(false); } var leafItem = getLatestVersion ? pageItem.items.OrderByDescending(registrationLeaf => new NugetPackageVersion(registrationLeaf.CatalogEntry.version)) .FirstOrDefault() : pageItem.items.Find(leaf => package.PackageVersion.InRange(new NugetPackageVersion(leaf.CatalogEntry.version))); if (leafItem is null) { Debug.LogError( $"There is no package with id '{package.Id}' and version '{package.PackageVersion}' in the registration page with the matching version rage: {pageItem.lower} to {pageItem.upper} '{pageItem.atId}'."); return null; } return leafItem; } [ItemNotNull] private async Task> GetRegistrationPageLeafItems( NugetPackageSourceV3 packageSource, RegistrationPageObject item, CancellationToken cancellationToken) { // If the items property is not present in the registration page object, // the URL specified in the @id must be used to fetch metadata about individual package versions. // The items array is sometimes excluded from the page object as an optimization. // If the number of versions of a single package ID is very large, then the registration index document will be massive and wasteful // to process for a client that only cares about a specific version or small range of versions. var itemAtId = item.atId ?? throw new InvalidOperationException($"missing '@id' for item inside item:\n{JsonUtility.ToJson(item)}"); var registrationPageString = await GetStringFromServerAsync(packageSource, itemAtId, cancellationToken).ConfigureAwait(false); var registrationPage = JsonUtility.FromJson(registrationPageString); return registrationPage.items ?? throw new InvalidOperationException( $"missing 'items' property inside page request for URL: {itemAtId}, response:\n{registrationPageString}"); } private async Task> GetRegistrationPageItemsAsync( NugetPackageSourceV3 packageSource, INugetPackageIdentifier package, CancellationToken cancellationToken) { var successfullyInitialized = await EnsureInitializedAsync(packageSource); if (!successfullyInitialized) { return null; } if (string.IsNullOrEmpty(registrationsBaseUrl)) { Debug.LogError( $"There are no {nameof(registrationsBaseUrl)} specified in the API '{apiIndexJsonUrl}' so we can't receive package details."); return null; } string responseString; try { responseString = await GetStringFromServerAsync( packageSource, $"{registrationsBaseUrl}{package.Id.ToLowerInvariant()}/index.json", cancellationToken) .ConfigureAwait(false); } catch (HttpRequestException exception) { Debug.LogWarning($"Failed to get '{package.Id}' from the package source '{apiIndexJsonUrl}'. Error: {exception}"); return null; } var registrationResponse = JsonUtility.FromJson(responseString.Replace(@"""@id"":", @"""atId"":")); // without a version specified, the latest version is returned var registrationItems = registrationResponse.items ?? throw new InvalidOperationException( $"missing 'items' property inside registration request for package: {package.Id}, response:\n{responseString}"); return registrationItems; } private void InitializeFromSessionState() { var sessionState = SessionState.GetString(GetSessionStateKey(), string.Empty); if (string.IsNullOrEmpty(sessionState)) { return; } JsonUtility.FromJsonOverwrite(sessionState, this); } private void SaveToSessionState() { UnityMainThreadDispatcher.Dispatch(() => SessionState.SetString(GetSessionStateKey(), JsonUtility.ToJson(this))); } private string GetSessionStateKey() { return $"{nameof(NugetApiClientV3)}:{apiIndexJsonUrl}"; } private async Task EnsureInitializedAsync(NugetPackageSourceV3 packageSource) { if (searchQueryServices != null) { return true; } try { var successful = await AwaitableInitializeApiAddressesAsync(packageSource); return successful; } catch (Exception exception) { Debug.LogError($"Initialization of api client for '{apiIndexJsonUrl}' failed so we can't use it. error (cached): {exception}."); return false; } } private Task AwaitableInitializeApiAddressesAsync(NugetPackageSourceV3 packageSource) { if (initializationTaskCompletionSource != null) { return initializationTaskCompletionSource.Task; } lock (httpClient) { if (initializationTaskCompletionSource != null) { return initializationTaskCompletionSource.Task; } initializationTaskCompletionSource = new TaskCompletionSource(); } _ = InitializeApiAddressesAsync(packageSource); return initializationTaskCompletionSource.Task; } private async Task InitializeApiAddressesAsync(NugetPackageSourceV3 packageSource) { Debug.Assert(initializationTaskCompletionSource != null, "initializationTaskCompletionSource != null"); try { var responseString = await GetStringFromServerAsync(packageSource, apiIndexJsonUrl.AbsoluteUri, CancellationToken.None) .ConfigureAwait(false); var resourceList = JsonUtility.FromJson( responseString.Replace(@"""@id"":", @"""atId"":").Replace(@"""@type"":", @"""atType"":")); var foundSearchQueryServices = new List(); var resources = resourceList.resources ?? throw new InvalidOperationException( $"missing '{nameof(resourceList.resources)}' property inside index response:\n{responseString}"); // we only support v3 so if v4 is released we skip it. var maxSupportedApiVersion = new NugetPackageVersion("4.0.0"); NugetPackageVersion highestPackageBaseAddressApiVersion = null; NugetPackageVersion highestRegistrationsBaseUrlApiVersion = null; NugetPackageVersion highestSearchQueryServiceApiVersion = null; foreach (var resource in resources) { var resourceAtId = resource.atId ?? throw new InvalidOperationException($"missing '@id' property inside resource of type '{resource.atType}'"); var resourceAtType = resource.atType ?? throw new InvalidOperationException($"missing '@type' property inside resource with id '{resource.atId}'"); var resourceTypeParts = resourceAtType.Split('/'); NugetPackageVersion resourceTypeVersion = null; // need to skip if version is no number like in: 'RegistrationsBaseUrl/Versioned' if (resourceTypeParts.Length > 1 && !string.IsNullOrEmpty(resourceTypeParts[1]) && char.IsDigit(resourceTypeParts[1][0])) { resourceTypeVersion = new NugetPackageVersion(resourceTypeParts[1]); } switch (resourceTypeParts[0]) { case "SearchQueryService": if (highestSearchQueryServiceApiVersion == null || (resourceTypeVersion > highestSearchQueryServiceApiVersion && resourceTypeVersion < maxSupportedApiVersion)) { highestSearchQueryServiceApiVersion = resourceTypeVersion; var comment = resource.comment ?? string.Empty; if (comment.IndexOf("(primary)", StringComparison.OrdinalIgnoreCase) >= 0) { foundSearchQueryServices.Insert(0, resourceAtId.Trim('/')); } else { foundSearchQueryServices.Add(resourceAtId.Trim('/')); } } break; case "PackageBaseAddress": if (highestPackageBaseAddressApiVersion == null || (resourceTypeVersion > highestPackageBaseAddressApiVersion && resourceTypeVersion < maxSupportedApiVersion)) { highestPackageBaseAddressApiVersion = resourceTypeVersion; packageBaseAddress = resourceAtId.Trim('/') + '/'; } break; case "RegistrationsBaseUrl": if (highestRegistrationsBaseUrlApiVersion == null || (resourceTypeVersion > highestRegistrationsBaseUrlApiVersion && resourceTypeVersion < maxSupportedApiVersion)) { highestRegistrationsBaseUrlApiVersion = resourceTypeVersion; registrationsBaseUrl = resourceAtId.Trim('/') + '/'; } break; } } if (!string.IsNullOrEmpty(PackageDownloadUrlTemplateOverwrite)) { packageDownloadUrlTemplate = PackageDownloadUrlTemplateOverwrite; } else if (string.IsNullOrEmpty(packageBaseAddress)) { UnityMainThreadDispatcher.Dispatch( () => { var displayDialog = EditorUtility.DisplayDialog( "Missing 'PackageBaseAddress' endpoint", $"The used NuGet V3 API '{apiIndexJsonUrl}' doesn't support the 'PackageBaseAddress' endpoint. You need to provide the url manually by specifying '{NugetConfigFile.PackageDownloadUrlTemplateOverwriteAttributeName}' on the package source. If this is a NuGet source provided by Artifactory you can click on 'Configure for Artifactory' to configure it.", "Configure for Artifactory", "Cancel"); if (displayDialog) { packageDownloadUrlTemplate = $"{registrationsBaseUrl}Download/{{0}}/{{1}}"; PackageDownloadUrlTemplateOverwrite = packageDownloadUrlTemplate; // Artifactory somehow can't handle search queries containing multiple packageId's. packageSource.UpdateSearchBatchSize = 1; SaveToSessionState(); ConfigurationManager.NugetConfigFile.Save(ConfigurationManager.NugetConfigFilePath); } else { Debug.LogErrorFormat( "The NuGet package source at '{0}' has no PackageBaseAddress resource defined, please specify it manually by adding the '{1}' attribute on the package-source configuration inside the '{2}' file.", apiIndexJsonUrl, NugetConfigFile.PackageDownloadUrlTemplateOverwriteAttributeName, NugetConfigFile.FileName); } }); } else { packageDownloadUrlTemplate = $"{packageBaseAddress}{{0}}/{{1}}/{{0}}.{{1}}.nupkg"; PackageDownloadUrlTemplateOverwrite = null; } searchQueryServices = foundSearchQueryServices; SaveToSessionState(); var successful = searchQueryServices.Count > 0; initializationTaskCompletionSource.SetResult(successful); } catch (Exception exception) { Debug.LogErrorFormat("Failed to initialize the NuGet package source '{0}'. Error: {1}", apiIndexJsonUrl, exception); initializationTaskCompletionSource.SetException(exception); } } private async Task GetStringFromServerAsync(NugetPackageSourceV3 packageSource, string url, CancellationToken cancellationToken) { using (var request = new HttpRequestMessage(HttpMethod.Get, url)) { AddHeadersToRequest(request, packageSource, true); using (var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false)) { await EnsureResponseIsSuccessAsync(response).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false) ?? string.Empty; return responseString; } } } private void AddHeadersToRequest(HttpRequestMessage request, NugetPackageSourceV3 packageSource, bool expectJsonResponse) { if (expectJsonResponse) { request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); } request.Headers.Add("User-Agent", "NuGetForUnity"); var password = packageSource.ExpandedPassword; var userName = packageSource.UserName; if (string.IsNullOrEmpty(password)) { var creds = CredentialProviderHelper.GetCredentialFromProvider(apiIndexJsonUrl); if (creds.HasValue) { userName = creds.Value.UserName; password = creds.Value.Password; } } if (!string.IsNullOrEmpty(password)) { request.Headers.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}"))); } } private sealed class QueryBuilder { private readonly StringBuilder builder = new StringBuilder(); public void Add(string parameterName, string parameterValue) { if (string.IsNullOrEmpty(parameterValue)) { return; } builder.Append(builder.Length == 0 ? '?' : '&'); builder.Append(parameterName); builder.Append('='); builder.Append(Uri.EscapeDataString(parameterValue)); } public override string ToString() { return builder.ToString(); } } // ReSharper disable InconsistentNaming // ReSharper disable UnassignedField.Local // ReSharper disable UnusedMember.Local // ReSharper disable NotAccessedField.Local #pragma warning disable CS0649 // Field is assigned on serialize #pragma warning disable CA1051 // Do not declare visible instance fields #pragma warning disable SA1307 // Accessible fields should begin with upper-case letter #pragma warning disable SA1401 // Fields should be private #pragma warning disable S1144 // Unused private types or members should be removed [Serializable] private sealed class IndexResponse { [CanBeNull] public List resources; [CanBeNull] public string version; } [Serializable] private sealed class Resource { [CanBeNull] public string atId; [CanBeNull] public string atType; [CanBeNull] public string clientVersion; [CanBeNull] public string comment; } [Serializable] private sealed class RegistrationResponse { /// /// The number of registration pages in the index. /// public int count; /// /// The array of registration pages. /// [CanBeNull] public List items; } [Serializable] private sealed class RegistrationPageObject { /// /// The URL to the registration page. /// If the items property is not present in the registration page object, the URL specified in the @id must be used to fetch metadata about /// individual package versions. /// [CanBeNull] public string atId; /// /// The number of registration leaves in the page. /// public int count; /// /// The array of registration leaves and their associate metadata. /// [CanBeNull] public List items; /// /// The lowest SemVer 2.0.0 version in the page (inclusive). /// [CanBeNull] public string lower; /// /// The highest SemVer 2.0.0 version in the page (inclusive). /// [CanBeNull] public string upper; } [Serializable] private sealed class RegistrationLeafObject { /// /// The URL to the registration leaf. /// [CanBeNull] public string atId; /// /// The catalog entry containing the package metadata. /// [CanBeNull] public CatalogEntry catalogEntry; /// /// The URL to the package content (.nupkg). /// [CanBeNull] public string packageContent; public CatalogEntry CatalogEntry => catalogEntry ?? throw new InvalidOperationException($"missing '{nameof(catalogEntry)}' property in registration leaf '{atId}'."); } // Removed property because it has a 'string or array of strings' representation and therefor causes parse errors when using the CLI: 'tags' [Serializable] private sealed class CatalogEntry { /// /// The URL to the document used to produce this object. /// [CanBeNull] public string atId; [CanBeNull] public string authors; /// /// The dependencies of the package, grouped by target framework. /// [CanBeNull] public List dependencyGroups; /// /// The deprecation associated with the package. /// [CanBeNull] public Deprecation deprecation; [CanBeNull] public string description; [CanBeNull] public string iconUrl; /// /// The ID of the package. /// [CanBeNull] public string id; [CanBeNull] public string language; [CanBeNull] public string licenseExpression; [CanBeNull] public string licenseUrl; /// /// Should be considered as listed if absent. /// public bool listed = true; [CanBeNull] public string minClientVersion; [CanBeNull] public string projectUrl; /// /// A string containing a ISO 8601 time-stamp of when the package was published. /// [CanBeNull] public string published; /// /// A URL for the rendered (HTML web page) view of the package README. /// [CanBeNull] public string readmeUrl; public bool requireLicenseAcceptance; [CanBeNull] public string summary; [CanBeNull] public string title; /// /// The full version string after normalization. /// [CanBeNull] public string version; /// /// The security vulnerabilities of the package. /// [CanBeNull] public List vulnerabilities; } [Serializable] private sealed class DependencyGroup { [CanBeNull] public List dependencies; /// /// The target framework that these dependencies are applicable to. /// public string targetFramework = string.Empty; } [Serializable] private sealed class Dependency { /// /// The ID of the package dependency. /// [CanBeNull] public string id; /// /// The allowed version range of the dependency. /// [CanBeNull] public string range; /// /// The URL to the registration index for this dependency. /// [CanBeNull] public string registration; } [Serializable] private sealed class Deprecation { /// /// The additional details about this deprecation. /// [CanBeNull] public string message; /// /// The reasons why the package was deprecated. /// [CanBeNull] public List reasons; } [Serializable] private sealed class Vulnerability { /// /// Location of security advisory for the package. /// [CanBeNull] public string advisoryUrl; /// /// Severity of advisory: "0" = Low, "1" = Moderate, "2" = High, "3" = Critical. /// [CanBeNull] public string severity; } [Serializable] private sealed class SearchResult { /// /// The search results matched by the request. /// [CanBeNull] public List data; /// /// The total number of matches, disregarding skip and take. /// public int totalHits; } [Serializable] private sealed class SearchResultItem { [CanBeNull] public List authors; [CanBeNull] public string description; [CanBeNull] public string iconUrl; /// /// The ID of the matched package. /// [CanBeNull] public string id; [CanBeNull] public string licenseUrl; [CanBeNull] public List owners; [CanBeNull] public string projectUrl; /// /// The absolute URL to the associated registration index. /// [CanBeNull] public string registration; [CanBeNull] public string summary; [CanBeNull] public List tags; [CanBeNull] public string title; /// /// This value can be inferred by the sum of downloads in the versions array. /// public long totalDownloads; /// /// A JSON boolean indicating whether the package is verified. /// public bool verified; /// /// The full SemVer 2.0.0 version string of the package (could contain build metadata). /// [CanBeNull] public string version; /// /// All of the versions of the package considering the prerelease parameter. /// [CanBeNull] public List versions; } [Serializable] private sealed class SearchResultVersion { /// /// The number of downloads for this specific package version. /// public long downloads; /// /// The full SemVer 2.0.0 version string of the package (could contain build metadata). /// [CanBeNull] public string version; } // ReSharper restore InconsistentNaming // ReSharper restore UnassignedField.Local // ReSharper restore UnusedMember.Local // ReSharper restore NotAccessedField.Local #pragma warning restore CS0649 // Field is assigned on serialize #pragma warning restore CA1051 // Do not declare visible instance fields #pragma warning restore SA1307 // Accessible fields should begin with upper-case letter #pragma warning restore SA1401 // Fields should be private #pragma warning restore S1144 // Unused private types or members should be removed } }