From 9c16b0068576495f647cc77c1c000124134444b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Sharma?= Date: Tue, 21 Jul 2020 21:19:07 -0700 Subject: [PATCH] Move to System.Text.Json (#559) Replaces Newtonsoft.Json with System.Text.Json. This will simplifies the route-to-code migration (see https://github.com/loic-sharma/BaGet/pull/558). --- .azure/pipelines/ci-official.yml | 2 +- .../Metadata/BaGetPackageMetadata.cs | 15 +- .../Metadata/BaGetRegistrationIndexPage.cs | 39 +++ .../BaGetRegistrationIndexPageItem.cs | 33 +++ .../BaGetRegistrationIndexResponse.cs | 39 ++- .../Metadata/BaGetRegistrationLeafResponse.cs | 15 -- .../Metadata/DefaultPackageMetadataService.cs | 3 +- .../Metadata/IPackageMetadataService.cs | 3 +- .../Metadata/RegistrationBuilder.cs | 11 +- src/BaGet.Core/Search/DependentsResponse.cs | 6 +- src/BaGet.Hosting/BaGet.Hosting.csproj | 4 - .../Controllers/PackageMetadataController.cs | 2 +- .../IEndpointRouteBuilderExtensions.cs | 17 -- .../IServiceCollectionExtensions.cs | 4 +- src/BaGet.Protocol/BaGet.Protocol.csproj | 2 +- .../Catalog/CatalogProcessor.cs | 23 +- src/BaGet.Protocol/Catalog/FileCursor.cs | 23 +- .../Catalog/RawCatalogClient.cs | 28 +-- .../Converters/BaseCatalogLeafConverter.cs | 35 --- .../CatalogLeafItemTypeConverter.cs | 42 ---- .../Converters/CatalogLeafTypeConverter.cs | 51 ---- .../PackageDependencyRangeConverter.cs | 33 --- .../PackageDependencyRangeJsonConverter.cs | 57 +++++ .../Converters/SingleOrListConverter.cs | 38 --- .../StringOrStringArrayJsonConverter.cs | 60 +++++ .../Extensions/CatalogModelExtensions.cs | 28 ++- .../Extensions/HttpClientExtensions.cs | 74 +++--- src/BaGet.Protocol/Models/AlternatePackage.cs | 11 +- .../Models/AutocompleteContext.cs | 4 +- .../Models/AutocompleteResponse.cs | 7 +- src/BaGet.Protocol/Models/CatalogIndex.cs | 8 +- src/BaGet.Protocol/Models/CatalogLeaf.cs | 21 +- src/BaGet.Protocol/Models/CatalogLeafItem.cs | 16 +- src/BaGet.Protocol/Models/CatalogLeafType.cs | 22 -- src/BaGet.Protocol/Models/CatalogPage.cs | 10 +- src/BaGet.Protocol/Models/CatalogPageItem.cs | 8 +- .../Models/DependencyGroupItem.cs | 6 +- src/BaGet.Protocol/Models/DependencyItem.cs | 8 +- src/BaGet.Protocol/Models/ICatalogLeafItem.cs | 5 - .../Models/PackageDeprecation.cs | 10 +- .../Models/PackageDetailsCatalogLeaf.cs | 52 ++-- src/BaGet.Protocol/Models/PackageMetadata.cs | 40 +-- .../Models/PackageVersionsResponse.cs | 4 +- .../Models/RegistrationIndexPage.cs | 12 +- .../Models/RegistrationIndexPageItem.cs | 8 +- .../Models/RegistrationIndexResponse.cs | 12 +- .../Models/RegistrationLeafResponse.cs | 14 +- .../Models/ResponseAndResult.cs | 50 ---- src/BaGet.Protocol/Models/SearchContext.cs | 6 +- src/BaGet.Protocol/Models/SearchResponse.cs | 8 +- src/BaGet.Protocol/Models/SearchResult.cs | 30 +-- .../Models/SearchResultVersion.cs | 8 +- src/BaGet.Protocol/Models/ServiceIndexItem.cs | 8 +- .../Models/ServiceIndexResponse.cs | 6 +- .../PackageContent/RawPackageContentClient.cs | 9 +- .../RawPackageMetadataClient.cs | 22 +- .../Search/RawAutocompleteClient.cs | 8 +- src/BaGet.Protocol/Search/RawSearchClient.cs | 4 +- .../ServiceIndex/RawServiceIndexClient.cs | 4 +- .../src/DisplayPackage/DisplayPackage.tsx | 2 +- .../src/DisplayPackage/Registration.tsx | 2 +- .../src/DisplayPackage/SourceRepository.tsx | 2 +- tests/BaGet.Core.Tests/Metadata/ModelTests.cs | 237 ++++++++++++++++++ ...ackageDependencyRangeJsonConverterTests.cs | 87 +++++++ .../StringOrStringArrayJsonConverterTests.cs | 123 +++++++++ .../RawCatalogClientTests.cs | 6 +- .../Support/TestDataHttpMessageHandler.cs | 5 +- tests/BaGet.Tests/ApiIntegrationTests.cs | 22 +- tests/BaGet.Tests/TestData.resx | 58 ++--- 69 files changed, 1012 insertions(+), 660 deletions(-) create mode 100644 src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs create mode 100644 src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs delete mode 100644 src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs delete mode 100644 src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs delete mode 100644 src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs create mode 100644 src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/SingleOrListConverter.cs create mode 100644 src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs delete mode 100644 src/BaGet.Protocol/Models/CatalogLeafType.cs delete mode 100644 src/BaGet.Protocol/Models/ResponseAndResult.cs create mode 100644 tests/BaGet.Core.Tests/Metadata/ModelTests.cs create mode 100644 tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs create mode 100644 tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs diff --git a/.azure/pipelines/ci-official.yml b/.azure/pipelines/ci-official.yml index eb2175de2..9c9b9726a 100644 --- a/.azure/pipelines/ci-official.yml +++ b/.azure/pipelines/ci-official.yml @@ -56,7 +56,7 @@ jobs: command: custom workingDir: src/BaGet.UI customCommand: run build - + - task: Npm@1 displayName: Test frontend inputs: diff --git a/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs b/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs index f6dba1d60..f84747d41 100644 --- a/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs +++ b/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using BaGet.Protocol.Models; -using Newtonsoft.Json; namespace BaGet.Core { @@ -10,26 +10,25 @@ namespace BaGet.Core /// public class BaGetPackageMetadata : PackageMetadata { - [JsonProperty("downloads")] + [JsonPropertyName("downloads")] public long Downloads { get; set; } - [JsonProperty("hasReadme")] + [JsonPropertyName("hasReadme")] public bool HasReadme { get; set; } - [JsonProperty("packageTypes")] + [JsonPropertyName("packageTypes")] public IReadOnlyList PackageTypes { get; set; } /// /// The package's release notes. /// - [JsonProperty("releaseNotes")] + [JsonPropertyName("releaseNotes")] public string ReleaseNotes { get; set; } - [JsonProperty("repositoryUrl")] + [JsonPropertyName("repositoryUrl")] public string RepositoryUrl { get; set; } - [JsonProperty("repositoryType")] + [JsonPropertyName("repositoryType")] public string RepositoryType { get; set; } - } } diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs new file mode 100644 index 000000000..6ade52d0f --- /dev/null +++ b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using BaGet.Protocol.Models; + +namespace BaGet.Core +{ + /// + /// BaGet's extensions to a registration index page. + /// Extends . + /// + /// + /// TODO: After this project is updated to .NET 5, make + /// extend and remove identical properties. + /// Properties that are modified should be marked with the "new" modified. + /// See: https://github.com/dotnet/runtime/pull/32107 + /// + public class BaGetRegistrationIndexPage + { +#region Original properties from RegistrationIndexPage. + [JsonPropertyName("@id")] + public string RegistrationPageUrl { get; set; } + + [JsonPropertyName("count")] + public int Count { get; set; } + + [JsonPropertyName("lower")] + public string Lower { get; set; } + + [JsonPropertyName("upper")] + public string Upper { get; set; } +#endregion + + /// + /// This was modified to use BaGet's extended registration index page item model. + /// + [JsonPropertyName("items")] + public IReadOnlyList ItemsOrNull { get; set; } + } +} diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs new file mode 100644 index 000000000..3177522e3 --- /dev/null +++ b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; +using BaGet.Protocol.Models; + +namespace BaGet.Core +{ + /// + /// BaGet's extensions to a registration index page. + /// Extends . + /// + /// + /// TODO: After this project is updated to .NET 5, make + /// extend and remove identical properties. + /// Properties that are modified should be marked with the "new" modified. + /// See: https://github.com/dotnet/runtime/pull/32107 + /// + public class BaGetRegistrationIndexPageItem + { +#region Original properties from RegistrationIndexPageItem. + [JsonPropertyName("@id")] + public string RegistrationLeafUrl { get; set; } + + [JsonPropertyName("packageContent")] + public string PackageContentUrl { get; set; } +#endregion + + /// + /// The catalog entry containing the package metadata. + /// This was modified to use BaGet's extended package metadata model. + /// + [JsonPropertyName("catalogEntry")] + public BaGetPackageMetadata PackageMetadata { get; set; } + } +} diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs b/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs index d82137755..9ff6ce66c 100644 --- a/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs +++ b/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs @@ -1,18 +1,45 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; using BaGet.Protocol.Models; -using Newtonsoft.Json; namespace BaGet.Core { /// - /// BaGet's extensions to a registration index response. These additions - /// are not part of the official protocol. + /// BaGet's extensions to a registration index response. + /// Extends . /// - public class BaGetRegistrationIndexResponse : RegistrationIndexResponse + /// + /// TODO: After this project is updated to .NET 5, make + /// extend and remove identical properties. + /// Properties that are modified should be marked with the "new" modified. + /// See: https://github.com/dotnet/runtime/pull/32107 + /// + public class BaGetRegistrationIndexResponse { +#region Original properties from RegistrationIndexResponse. + [JsonPropertyName("@id")] + public string RegistrationIndexUrl { get; set; } + + [JsonPropertyName("@type")] + public IReadOnlyList Type { get; set; } + + [JsonPropertyName("count")] + public int Count { get; set; } +#endregion + + /// + /// The pages that contain all of the versions of the package, ordered + /// by the package's version. This was modified to use BaGet's extended + /// registration index page model. + /// + [JsonPropertyName("items")] + public IReadOnlyList Pages { get; set; } + /// - /// How many times all versions of this package have been downloaded. + /// The package's total downloads across all versions. + /// This is not part of the official NuGet protocol. /// - [JsonProperty("totalDownloads")] + [JsonPropertyName("totalDownloads")] public long TotalDownloads { get; set; } } } diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs b/src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs deleted file mode 100644 index 4e76a6e0e..000000000 --- a/src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs +++ /dev/null @@ -1,15 +0,0 @@ -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Core -{ - /// - /// BaGet's extensions to a registration leaf response. These additions - /// are not part of the official protocol. - /// - public class BaGetRegistrationLeafResponse : RegistrationLeafResponse - { - [JsonProperty("downloads")] - public long Downloads { get; set; } - } -} diff --git a/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs b/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs index d9804a092..4196ba74e 100644 --- a/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs +++ b/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using BaGet.Protocol.Models; using NuGet.Packaging.Core; using NuGet.Versioning; @@ -41,7 +42,7 @@ public async Task GetRegistrationIndexOrNullAsyn packages)); } - public async Task GetRegistrationLeafOrNullAsync( + public async Task GetRegistrationLeafOrNullAsync( string id, NuGetVersion version, CancellationToken cancellationToken = default) diff --git a/src/BaGet.Core/Metadata/IPackageMetadataService.cs b/src/BaGet.Core/Metadata/IPackageMetadataService.cs index 50cfb94e7..83bf3fab0 100644 --- a/src/BaGet.Core/Metadata/IPackageMetadataService.cs +++ b/src/BaGet.Core/Metadata/IPackageMetadataService.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using BaGet.Protocol.Models; using NuGet.Versioning; namespace BaGet.Core @@ -27,7 +28,7 @@ public interface IPackageMetadataService /// The package's version. /// A token to cancel the task. /// The registration leaf, or null if the package does not exist. - Task GetRegistrationLeafOrNullAsync( + Task GetRegistrationLeafOrNullAsync( string packageId, NuGetVersion packageVersion, CancellationToken cancellationToken = default); diff --git a/src/BaGet.Core/Metadata/RegistrationBuilder.cs b/src/BaGet.Core/Metadata/RegistrationBuilder.cs index 515d16f60..4090e9323 100644 --- a/src/BaGet.Core/Metadata/RegistrationBuilder.cs +++ b/src/BaGet.Core/Metadata/RegistrationBuilder.cs @@ -29,7 +29,7 @@ public virtual BaGetRegistrationIndexResponse BuildIndex(PackageRegistration reg TotalDownloads = registration.Packages.Sum(p => p.Downloads), Pages = new[] { - new RegistrationIndexPage + new BaGetRegistrationIndexPage { RegistrationPageUrl = _url.GetRegistrationIndexUrl(registration.PackageId), Count = registration.Packages.Count(), @@ -41,16 +41,15 @@ public virtual BaGetRegistrationIndexResponse BuildIndex(PackageRegistration reg }; } - public virtual BaGetRegistrationLeafResponse BuildLeaf(Package package) + public virtual RegistrationLeafResponse BuildLeaf(Package package) { var id = package.Id; var version = package.Version; - return new BaGetRegistrationLeafResponse + return new RegistrationLeafResponse { Type = RegistrationLeafResponse.DefaultType, Listed = package.Listed, - Downloads = package.Downloads, Published = package.Published, RegistrationLeafUrl = _url.GetRegistrationLeafUrl(id, version), PackageContentUrl = _url.GetPackageDownloadUrl(id, version), @@ -58,8 +57,8 @@ public virtual BaGetRegistrationLeafResponse BuildLeaf(Package package) }; } - private RegistrationIndexPageItem ToRegistrationIndexPageItem(Package package) => - new RegistrationIndexPageItem + private BaGetRegistrationIndexPageItem ToRegistrationIndexPageItem(Package package) => + new BaGetRegistrationIndexPageItem { RegistrationLeafUrl = _url.GetRegistrationLeafUrl(package.Id, package.Version), PackageContentUrl = _url.GetPackageDownloadUrl(package.Id, package.Version), diff --git a/src/BaGet.Core/Search/DependentsResponse.cs b/src/BaGet.Core/Search/DependentsResponse.cs index c25a65530..cf01b677e 100644 --- a/src/BaGet.Core/Search/DependentsResponse.cs +++ b/src/BaGet.Core/Search/DependentsResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Core { @@ -12,13 +12,13 @@ public class DependentsResponse /// /// The total number of matches, disregarding skip and take. /// - [JsonProperty("totalHits")] + [JsonPropertyName("totalHits")] public long TotalHits { get; set; } /// /// The package IDs matched by the dependent query. /// - [JsonProperty("data")] + [JsonPropertyName("data")] public IReadOnlyList Data { get; set; } } } diff --git a/src/BaGet.Hosting/BaGet.Hosting.csproj b/src/BaGet.Hosting/BaGet.Hosting.csproj index 46e5ad4f3..68850c33a 100644 --- a/src/BaGet.Hosting/BaGet.Hosting.csproj +++ b/src/BaGet.Hosting/BaGet.Hosting.csproj @@ -11,10 +11,6 @@ - - - - diff --git a/src/BaGet.Hosting/Controllers/PackageMetadataController.cs b/src/BaGet.Hosting/Controllers/PackageMetadataController.cs index ded05cbef..0707630c7 100644 --- a/src/BaGet.Hosting/Controllers/PackageMetadataController.cs +++ b/src/BaGet.Hosting/Controllers/PackageMetadataController.cs @@ -23,7 +23,7 @@ public PackageMetadataController(IPackageMetadataService metadata) // GET v3/registration/{id}.json [HttpGet] - public async Task> RegistrationIndexAsync(string id, CancellationToken cancellationToken) + public async Task> RegistrationIndexAsync(string id, CancellationToken cancellationToken) { var index = await _metadata.GetRegistrationIndexOrNullAsync(id, cancellationToken); if (index == null) diff --git a/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs b/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs deleted file mode 100644 index 79a90c43a..000000000 --- a/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using BaGet.Hosting; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Constraints; - -namespace BaGet -{ - public static class IEndpointRouteBuilderExtensions - { - public static void MapBaGetRoutes(this IEndpointRouteBuilder endpoints) - { - - } - - - } -} diff --git a/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs b/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs index 57670e1b9..5ec8134a8 100644 --- a/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs +++ b/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs @@ -17,9 +17,9 @@ public static IServiceCollection AddBaGetWebApplication( .AddControllers() .AddApplicationPart(typeof(PackageContentController).Assembly) .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) - .AddNewtonsoftJson(options => + .AddJsonOptions(options => { - options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + options.JsonSerializerOptions.IgnoreNullValues = true; }); services.AddHttpContextAccessor(); diff --git a/src/BaGet.Protocol/BaGet.Protocol.csproj b/src/BaGet.Protocol/BaGet.Protocol.csproj index f08bbe059..5711a54bb 100644 --- a/src/BaGet.Protocol/BaGet.Protocol.csproj +++ b/src/BaGet.Protocol/BaGet.Protocol.csproj @@ -9,9 +9,9 @@ - + diff --git a/src/BaGet.Protocol/Catalog/CatalogProcessor.cs b/src/BaGet.Protocol/Catalog/CatalogProcessor.cs index 684d6ddb4..13c5cc274 100644 --- a/src/BaGet.Protocol/Catalog/CatalogProcessor.cs +++ b/src/BaGet.Protocol/Catalog/CatalogProcessor.cs @@ -144,18 +144,19 @@ private async Task ProcessLeafAsync(CatalogLeafItem leafItem, Cancellation bool success; try { - switch (leafItem.Type) + if (leafItem.IsPackageDelete()) { - case CatalogLeafType.PackageDelete: - var packageDelete = await _client.GetPackageDeleteLeafAsync(leafItem.CatalogLeafUrl); - success = await _leafProcessor.ProcessPackageDeleteAsync(packageDelete, cancellationToken); - break; - case CatalogLeafType.PackageDetails: - var packageDetails = await _client.GetPackageDetailsLeafAsync(leafItem.CatalogLeafUrl); - success = await _leafProcessor.ProcessPackageDetailsAsync(packageDetails, cancellationToken); - break; - default: - throw new NotSupportedException($"The catalog leaf type '{leafItem.Type}' is not supported."); + var packageDelete = await _client.GetPackageDeleteLeafAsync(leafItem.CatalogLeafUrl); + success = await _leafProcessor.ProcessPackageDeleteAsync(packageDelete, cancellationToken); + } + else if (leafItem.IsPackageDetails()) + { + var packageDetails = await _client.GetPackageDetailsLeafAsync(leafItem.CatalogLeafUrl); + success = await _leafProcessor.ProcessPackageDetailsAsync(packageDetails, cancellationToken); + } + else + { + throw new NotSupportedException($"The catalog leaf type '{leafItem.Type}' is not supported."); } } catch (Exception exception) diff --git a/src/BaGet.Protocol/Catalog/FileCursor.cs b/src/BaGet.Protocol/Catalog/FileCursor.cs index b1b6a9ddd..9be09c2c9 100644 --- a/src/BaGet.Protocol/Catalog/FileCursor.cs +++ b/src/BaGet.Protocol/Catalog/FileCursor.cs @@ -1,9 +1,10 @@ using System; using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace BaGet.Protocol.Catalog { @@ -14,8 +15,6 @@ namespace BaGet.Protocol.Catalog /// public class FileCursor : ICursor { - private static readonly JsonSerializerSettings Settings = HttpClientExtensions.JsonSettings; - private readonly string _path; private readonly ILogger _logger; @@ -25,25 +24,27 @@ public FileCursor(string path, ILogger logger) _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public Task GetAsync(CancellationToken cancellationToken) + public async Task GetAsync(CancellationToken cancellationToken) { try { - var jsonString = File.ReadAllText(_path); - var data = JsonConvert.DeserializeObject(jsonString, Settings); - _logger.LogDebug("Read cursor value {cursor:O} from {path}.", data.Value, _path); - return Task.FromResult(data.Value); + using (var file = File.OpenRead(_path)) + { + var data = await JsonSerializer.DeserializeAsync(file, options: null, cancellationToken); + _logger.LogDebug("Read cursor value {cursor:O} from {path}.", data.Value, _path); + return data.Value; + } } catch (Exception e) when (e is FileNotFoundException || e is JsonException) { - return Task.FromResult(null); + return null; } } public Task SetAsync(DateTimeOffset value, CancellationToken cancellationToken) { var data = new Data { Value = value }; - var jsonString = JsonConvert.SerializeObject(data); + var jsonString = JsonSerializer.Serialize(data); File.WriteAllText(_path, jsonString); _logger.LogDebug("Wrote cursor value {cursor:O} to {path}.", data.Value, _path); return Task.CompletedTask; @@ -51,7 +52,7 @@ public Task SetAsync(DateTimeOffset value, CancellationToken cancellationToken) private class Data { - [JsonProperty("value")] + [JsonPropertyName("value")] public DateTimeOffset Value { get; set; } } } diff --git a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs index e5feb6572..d773e3654 100644 --- a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs +++ b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -19,22 +20,18 @@ public RawCatalogClient(HttpClient httpClient, string catalogUrl) public async Task GetIndexAsync(CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(_catalogUrl, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(_catalogUrl, cancellationToken); } public async Task GetPageAsync(string pageUrl, CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(pageUrl, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(pageUrl, cancellationToken); } public async Task GetPackageDeleteLeafAsync(string leafUrl, CancellationToken cancellationToken = default) { return await GetAndValidateLeafAsync( - CatalogLeafType.PackageDelete, + "PackageDelete", leafUrl, cancellationToken); } @@ -42,24 +39,23 @@ public async Task GetPackageDeleteLeafAsync(string lea public async Task GetPackageDetailsLeafAsync(string leafUrl, CancellationToken cancellationToken = default) { return await GetAndValidateLeafAsync( - CatalogLeafType.PackageDetails, + "PackageDetails", leafUrl, cancellationToken); } - private async Task GetAndValidateLeafAsync( - CatalogLeafType type, + private async Task GetAndValidateLeafAsync( + string leafType, string leafUrl, - CancellationToken cancellationToken) where T : CatalogLeaf + CancellationToken cancellationToken) where TCatalogLeaf : CatalogLeaf { - var result = await _httpClient.DeserializeUrlAsync(leafUrl, cancellationToken); - var leaf = result.GetResultOrThrow(); + var leaf = await _httpClient.GetFromJsonAsync(leafUrl, cancellationToken); - if (leaf.Type != type) + if (leaf.Type.FirstOrDefault() != leafType) { throw new ArgumentException( - $"The leaf type found in the document does not match the expected '{type}' type.", - nameof(type)); + $"The leaf type found in the document does not match the expected '{leafType}' type.", + nameof(leafType)); } return leaf; diff --git a/src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs b/src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs deleted file mode 100644 index 70ea04cd6..000000000 --- a/src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/BaseCatalogLeafConverter.cs - /// - internal abstract class BaseCatalogLeafConverter : JsonConverter - { - private readonly IReadOnlyDictionary _fromType; - - public BaseCatalogLeafConverter(IReadOnlyDictionary fromType) - { - _fromType = fromType; - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(CatalogLeafType); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (!_fromType.TryGetValue((CatalogLeafType)value, out var output)) - { - throw new NotSupportedException($"The catalog leaf type '{value}' is not supported."); - } - - writer.WriteValue(output); - } - } -} diff --git a/src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs b/src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs deleted file mode 100644 index a31c26e40..000000000 --- a/src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/CatalogLeafItemTypeConverter.cs - /// - internal class CatalogLeafItemTypeConverter : BaseCatalogLeafConverter - { - private static readonly Dictionary FromType = new Dictionary - { - { CatalogLeafType.PackageDelete, "nuget:PackageDelete" }, - { CatalogLeafType.PackageDetails, "nuget:PackageDetails" }, - }; - - private static readonly Dictionary FromString = FromType - .ToDictionary(x => x.Value, x => x.Key); - - public CatalogLeafItemTypeConverter() : base(FromType) - { - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var stringValue = reader.Value as string; - if (stringValue != null) - { - CatalogLeafType output; - if (FromString.TryGetValue(stringValue, out output)) - { - return output; - } - } - - throw new JsonSerializationException($"Unexpected value for a {nameof(CatalogLeafType)}."); - } - } -} diff --git a/src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs b/src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs deleted file mode 100644 index bc37acf16..000000000 --- a/src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/CatalogLeafTypeConverter.cs - /// - internal class CatalogLeafTypeConverter : BaseCatalogLeafConverter - { - private static readonly Dictionary FromType = new Dictionary - { - { CatalogLeafType.PackageDelete, "PackageDelete" }, - { CatalogLeafType.PackageDetails, "PackageDetails" }, - }; - - private static readonly Dictionary FromString = FromType - .ToDictionary(x => x.Value, x => x.Key); - - public CatalogLeafTypeConverter() : base(FromType) - { - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - List types; - if (reader.TokenType == JsonToken.StartArray) - { - types = serializer.Deserialize>(reader); - } - else - { - types = new List { reader.Value }; - } - - foreach (var type in types.OfType()) - { - CatalogLeafType output; - if (FromString.TryGetValue(type, out output)) - { - return output; - } - } - - throw new JsonSerializationException($"Unexpected value for a {nameof(CatalogLeafType)}."); - } - } -} diff --git a/src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs b/src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs deleted file mode 100644 index f199cd608..000000000 --- a/src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - internal class PackageDependencyRangeConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(string); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.StartArray) - { - // There are some quirky packages with arrays of dependency version ranges. In this case, we take the - // first element. - // Example: https://api.nuget.org/v3/catalog0/data/2016.02.21.11.06.01/dingu.generic.repo.ef7.1.0.0-beta2.json - var array = serializer.Deserialize(reader); - return array.FirstOrDefault(); - } - - return serializer.Deserialize(reader); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - serializer.Serialize(writer, value); - } - } -} diff --git a/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs b/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs new file mode 100644 index 000000000..9514cb5d5 --- /dev/null +++ b/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BaGet.Protocol.Internal +{ + /// + /// This is an internal API that may be changed or removed without notice in any release. + /// + public class PackageDependencyRangeJsonConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString(); + } + + // There are some quirky packages with arrays of dependency version ranges. + // In this case, we take the first element. + // Example: https://api.nuget.org/v3/catalog0/data/2016.02.21.11.06.01/dingu.generic.repo.ef7.1.0.0-beta2.json + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var result = reader.GetString(); + + // Ignore all other strings until we reach the end of the array. + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } +} diff --git a/src/BaGet.Protocol/Converters/SingleOrListConverter.cs b/src/BaGet.Protocol/Converters/SingleOrListConverter.cs deleted file mode 100644 index d0f8537ab..000000000 --- a/src/BaGet.Protocol/Converters/SingleOrListConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Converts a single value or a list of values into the desired type or . - /// - /// The desired type. - internal class SingleOrListConverter : JsonConverter - { - /// - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IReadOnlyList); - } - - /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.StartArray) - { - return serializer.Deserialize>(reader); - } - else - { - return serializer.Deserialize(reader); - } - } - - /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - serializer.Serialize(writer, value); - } - } -} diff --git a/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs b/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs new file mode 100644 index 000000000..d4ea42e04 --- /dev/null +++ b/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BaGet.Protocol.Internal +{ + /// + /// This is an internal API that may be changed or removed without notice in any release. + /// + public class StringOrStringArrayJsonConverter : JsonConverter> + { + public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Try to read a single string first. + if (reader.TokenType == JsonTokenType.String) + { + return new List { reader.GetString() }; + } + + // Try to read an array of strings. + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + var result = new List(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.String) + { + result.Add(reader.GetString()); + } + else if (reader.TokenType == JsonTokenType.EndArray) + { + return result; + } + else + { + break; + } + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList values, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (var value in values) + { + writer.WriteStringValue(value); + } + + writer.WriteEndArray(); + } + } +} diff --git a/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs b/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs index adff9c424..66f18ffd3 100644 --- a/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs +++ b/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs @@ -142,9 +142,29 @@ public static VersionRange ParseRange(this DependencyItem packageDependency) /// /// The catalog leaf. /// True if the catalog leaf represents a package delete. - public static bool IsPackageDelete(this ICatalogLeafItem leaf) + public static bool IsPackageDelete(this CatalogLeafItem leaf) { - return leaf.Type == CatalogLeafType.PackageDelete; + return leaf.Type == "nuget:PackageDelete"; + } + + /// + /// Determines if the provided catalog leaf is a package delete. + /// + /// The catalog leaf. + /// True if the catalog leaf represents a package delete. + public static bool IsPackageDelete(this CatalogLeaf leaf) + { + return leaf.Type.FirstOrDefault() == "PackageDelete"; + } + + /// + /// Determines if the provided catalog leaf is contains package details. + /// + /// The catalog leaf. + /// True if the catalog leaf contains package details. + public static bool IsPackageDetails(this CatalogLeafItem leaf) + { + return leaf.Type == "nuget:PackageDetails"; } /// @@ -152,9 +172,9 @@ public static bool IsPackageDelete(this ICatalogLeafItem leaf) /// /// The catalog leaf. /// True if the catalog leaf contains package details. - public static bool IsPackageDetails(this ICatalogLeafItem leaf) + public static bool IsPackageDetails(this CatalogLeaf leaf) { - return leaf.Type == CatalogLeafType.PackageDetails; + return leaf.Type.FirstOrDefault() == "PackageDetails"; } /// diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index 805456cc3..572d70119 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -1,55 +1,71 @@ -using System.IO; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; namespace BaGet.Protocol { internal static class HttpClientExtensions { - internal static readonly JsonSerializer Serializer = JsonSerializer.Create(JsonSettings); - - internal static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + /// + /// Deserialize JSON content. + /// + /// The JSON type to deserialize. + /// The HTTP client that will perform the request. + /// The request URI. + /// A token to cancel the task. + /// The deserialized JSON content + public static async Task GetFromJsonAsync( + this HttpClient httpClient, + string requestUri, + CancellationToken cancellationToken = default) { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - DateParseHandling = DateParseHandling.DateTimeOffset, - NullValueHandling = NullValueHandling.Ignore, - }; + using (var response = await httpClient.GetAsync( + requestUri, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken)) + { + // This is similar to System.Net.Http.Json's implementation, however, + // this does not validate that the response's content type indicates JSON content. + response.EnsureSuccessStatusCode(); - public static async Task> DeserializeUrlAsync( + using (var stream = await response.Content.ReadAsStreamAsync()) + { + return await JsonSerializer.DeserializeAsync(stream); + } + } + } + + /// + /// Deserialize JSON content. If the HTTP response status code is 404, + /// returns the default value. + /// + /// The JSON type to deserialize. + /// The HTTP client that will perform the request. + /// The request URI. + /// A token to cancel the task. + /// The JSON content, or, the default value if the HTTP response status code is 404. + public static async Task GetFromJsonOrDefaultAsync( this HttpClient httpClient, - string documentUrl, + string requestUri, CancellationToken cancellationToken = default) { using (var response = await httpClient.GetAsync( - documentUrl, + requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) { - if (response.StatusCode != HttpStatusCode.OK) + if (response.StatusCode == HttpStatusCode.NotFound) { - return new ResponseAndResult( - HttpMethod.Get, - documentUrl, - response.StatusCode, - response.ReasonPhrase, - hasResult: false, - result: default); + return default; } + response.EnsureSuccessStatusCode(); + using (var stream = await response.Content.ReadAsStreamAsync()) - using (var textReader = new StreamReader(stream)) - using (var jsonReader = new JsonTextReader(textReader)) { - return new ResponseAndResult( - HttpMethod.Get, - documentUrl, - response.StatusCode, - response.ReasonPhrase, - hasResult: true, - result: Serializer.Deserialize(jsonReader)); + return await JsonSerializer.DeserializeAsync(stream); } } } diff --git a/src/BaGet.Protocol/Models/AlternatePackage.cs b/src/BaGet.Protocol/Models/AlternatePackage.cs index 6699c2405..a5e272adc 100644 --- a/src/BaGet.Protocol/Models/AlternatePackage.cs +++ b/src/BaGet.Protocol/Models/AlternatePackage.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -10,22 +9,22 @@ namespace BaGet.Protocol.Models /// public class AlternatePackage { - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string Url { get; set; } - [JsonProperty("@type")] + [JsonPropertyName("@type")] public string Type { get; set; } /// /// The ID of the alternate package. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id { get; set; } /// /// The allowed version range, or * if any version is allowed. /// - [JsonProperty("range")] + [JsonPropertyName("range")] public string Range { get; set; } } } diff --git a/src/BaGet.Protocol/Models/AutocompleteContext.cs b/src/BaGet.Protocol/Models/AutocompleteContext.cs index b499be46f..96b711c2f 100644 --- a/src/BaGet.Protocol/Models/AutocompleteContext.cs +++ b/src/BaGet.Protocol/Models/AutocompleteContext.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -9,7 +9,7 @@ public class AutocompleteContext Vocab = "http://schema.nuget.org/schema#" }; - [JsonProperty("@vocab")] + [JsonPropertyName("@vocab")] public string Vocab { get; set; } } } diff --git a/src/BaGet.Protocol/Models/AutocompleteResponse.cs b/src/BaGet.Protocol/Models/AutocompleteResponse.cs index ec186c409..5522656fe 100644 --- a/src/BaGet.Protocol/Models/AutocompleteResponse.cs +++ b/src/BaGet.Protocol/Models/AutocompleteResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -10,18 +10,19 @@ namespace BaGet.Protocol.Models /// public class AutocompleteResponse { + [JsonPropertyName("@context")] public AutocompleteContext Context { get; set; } /// /// The total number of matches, disregarding skip and take. /// - [JsonProperty("totalHits")] + [JsonPropertyName("totalHits")] public long TotalHits { get; set; } /// /// The package IDs matched by the autocomplete query. /// - [JsonProperty("data")] + [JsonPropertyName("data")] public IReadOnlyList Data { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogIndex.cs b/src/BaGet.Protocol/Models/CatalogIndex.cs index fa6664b28..beeea373e 100644 --- a/src/BaGet.Protocol/Models/CatalogIndex.cs +++ b/src/BaGet.Protocol/Models/CatalogIndex.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,19 +17,19 @@ public class CatalogIndex /// /// A timestamp of the most recent commit. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The number of catalog pages in the catalog index. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// The items used to discover s. /// - [JsonProperty("items")] + [JsonPropertyName("items")] public List Items { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogLeaf.cs b/src/BaGet.Protocol/Models/CatalogLeaf.cs index cb6476006..f41ce62b9 100644 --- a/src/BaGet.Protocol/Models/CatalogLeaf.cs +++ b/src/BaGet.Protocol/Models/CatalogLeaf.cs @@ -1,6 +1,6 @@ using System; -using BaGet.Protocol.Internal; -using Newtonsoft.Json; +using System.Collections.Generic; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,44 +17,43 @@ public class CatalogLeaf : ICatalogLeafItem /// /// The URL to the current catalog leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The type of the current catalog leaf. /// - [JsonProperty("@type")] - [JsonConverter(typeof(CatalogLeafTypeConverter))] - public CatalogLeafType Type { get; set; } + [JsonPropertyName("@type")] + public IReadOnlyList Type { get; set; } /// /// The catalog commit ID associated with this catalog item. /// - [JsonProperty("catalog:commitId")] + [JsonPropertyName("catalog:commitId")] public string CommitId { get; set; } /// /// The commit timestamp of this catalog item. /// - [JsonProperty("catalog:commitTimeStamp")] + [JsonPropertyName("catalog:commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The package ID of the catalog item. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string PackageId { get; set; } /// /// The published date of the package catalog item. /// - [JsonProperty("published")] + [JsonPropertyName("published")] public DateTimeOffset Published { get; set; } /// /// The package version of the catalog item. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string PackageVersion { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogLeafItem.cs b/src/BaGet.Protocol/Models/CatalogLeafItem.cs index a18441470..36dcb940e 100644 --- a/src/BaGet.Protocol/Models/CatalogLeafItem.cs +++ b/src/BaGet.Protocol/Models/CatalogLeafItem.cs @@ -1,6 +1,5 @@ using System; -using BaGet.Protocol.Internal; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -16,32 +15,31 @@ public class CatalogLeafItem : ICatalogLeafItem /// /// The URL to the current catalog leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The type of the current catalog leaf. /// - [JsonProperty("@type")] - [JsonConverter(typeof(CatalogLeafItemTypeConverter))] - public CatalogLeafType Type { get; set; } + [JsonPropertyName("@type")] + public string Type { get; set; } /// /// The commit timestamp of this catalog item. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The package ID of the catalog item. /// - [JsonProperty("nuget:id")] + [JsonPropertyName("nuget:id")] public string PackageId { get; set; } /// /// The package version of the catalog item. /// - [JsonProperty("nuget:version")] + [JsonPropertyName("nuget:version")] public string PackageVersion { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogLeafType.cs b/src/BaGet.Protocol/Models/CatalogLeafType.cs deleted file mode 100644 index b158a2393..000000000 --- a/src/BaGet.Protocol/Models/CatalogLeafType.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace BaGet.Protocol.Models -{ - // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogLeafType.cs - - /// - /// The type of a . - /// - /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#item-types - /// - public enum CatalogLeafType - { - /// - /// The represents the snapshot of a package's metadata. - /// - PackageDetails = 1, - - /// - /// The represents a package that was deleted. - /// - PackageDelete = 2, - } -} diff --git a/src/BaGet.Protocol/Models/CatalogPage.cs b/src/BaGet.Protocol/Models/CatalogPage.cs index 3ad6f1edc..65f9e6d3f 100644 --- a/src/BaGet.Protocol/Models/CatalogPage.cs +++ b/src/BaGet.Protocol/Models/CatalogPage.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,25 +17,25 @@ public class CatalogPage /// /// A unique ID associated with the most recent commit in this page. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The number of items in the page. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// The items used to discover s. /// - [JsonProperty("items")] + [JsonPropertyName("items")] public List Items { get; set; } /// /// The URL to the Catalog Index. /// - [JsonProperty("parent")] + [JsonPropertyName("parent")] public string CatalogIndexUrl { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogPageItem.cs b/src/BaGet.Protocol/Models/CatalogPageItem.cs index 4d375d221..0240d9e23 100644 --- a/src/BaGet.Protocol/Models/CatalogPageItem.cs +++ b/src/BaGet.Protocol/Models/CatalogPageItem.cs @@ -1,5 +1,5 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -15,19 +15,19 @@ public class CatalogPageItem /// /// The URL to this item's corresponding . /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogPageUrl { get; set; } /// /// A timestamp of the most recent commit in this page. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The number of items in the page. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } } } diff --git a/src/BaGet.Protocol/Models/DependencyGroupItem.cs b/src/BaGet.Protocol/Models/DependencyGroupItem.cs index a5fca2fde..e9626c053 100644 --- a/src/BaGet.Protocol/Models/DependencyGroupItem.cs +++ b/src/BaGet.Protocol/Models/DependencyGroupItem.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,13 +13,13 @@ public class DependencyGroupItem /// /// The target framework that these dependencies are applicable to. /// - [JsonProperty("targetFramework")] + [JsonPropertyName("targetFramework")] public string TargetFramework { get; set; } /// /// A list of dependencies. /// - [JsonProperty("dependencies", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("dependencies")] public List Dependencies { get; set; } } } diff --git a/src/BaGet.Protocol/Models/DependencyItem.cs b/src/BaGet.Protocol/Models/DependencyItem.cs index b197bf117..95af13012 100644 --- a/src/BaGet.Protocol/Models/DependencyItem.cs +++ b/src/BaGet.Protocol/Models/DependencyItem.cs @@ -1,5 +1,5 @@ +using System.Text.Json.Serialization; using BaGet.Protocol.Internal; -using Newtonsoft.Json; namespace BaGet.Protocol.Models { @@ -13,14 +13,14 @@ public class DependencyItem /// /// The ID of the package dependency. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id { get; set; } /// /// The allowed version range of the dependency. /// - [JsonProperty("range")] - [JsonConverter(typeof(PackageDependencyRangeConverter))] + [JsonPropertyName("range")] + [JsonConverter(typeof(PackageDependencyRangeJsonConverter))] public string Range { get; set; } } } diff --git a/src/BaGet.Protocol/Models/ICatalogLeafItem.cs b/src/BaGet.Protocol/Models/ICatalogLeafItem.cs index 99bcb5468..2cd59a37f 100644 --- a/src/BaGet.Protocol/Models/ICatalogLeafItem.cs +++ b/src/BaGet.Protocol/Models/ICatalogLeafItem.cs @@ -26,10 +26,5 @@ public interface ICatalogLeafItem /// The package version of the catalog item. /// string PackageVersion { get; } - - /// - /// The type of the current catalog leaf. - /// - CatalogLeafType Type { get; } } } diff --git a/src/BaGet.Protocol/Models/PackageDeprecation.cs b/src/BaGet.Protocol/Models/PackageDeprecation.cs index c00a986c3..9445ade2d 100644 --- a/src/BaGet.Protocol/Models/PackageDeprecation.cs +++ b/src/BaGet.Protocol/Models/PackageDeprecation.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,26 +13,26 @@ public class PackageDeprecation /// /// The URL to the document used to produce this object. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The reasons why the package was deprecated. /// Deprecation reasons include: "Legacy", "CriticalBugs", and "Other". /// - [JsonProperty("reasons")] + [JsonPropertyName("reasons")] public IReadOnlyList Reasons { get; set; } /// /// The additional details about this deprecation. /// - [JsonProperty("message")] + [JsonPropertyName("message")] public string Message { get; set; } /// /// The alternate package that should be used instead. /// - [JsonProperty("alternatePackage")] + [JsonPropertyName("alternatePackage")] public AlternatePackage AlternatePackage { get; set; } } } diff --git a/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs b/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs index 0f67b6576..5e8cf2575 100644 --- a/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs +++ b/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,43 +17,43 @@ public class PackageDetailsCatalogLeaf : CatalogLeaf /// /// The package's authors. /// - [JsonProperty("authors")] + [JsonPropertyName("authors")] public string Authors { get; set; } /// /// The package's copyright. /// - [JsonProperty("copyright")] + [JsonPropertyName("copyright")] public string Copyright { get; set; } /// /// A timestamp of when the package was first created. Fallback property: . /// - [JsonProperty("created")] + [JsonPropertyName("created")] public DateTimeOffset Created { get; set; } /// /// A timestamp of when the package was last edited. /// - [JsonProperty("lastEdited")] + [JsonPropertyName("lastEdited")] public DateTimeOffset LastEdited { get; set; } /// /// The dependencies of the package, grouped by target framework. /// - [JsonProperty("dependencyGroups")] + [JsonPropertyName("dependencyGroups")] public List DependencyGroups { get; set; } /// /// The package's description. /// - [JsonProperty("description")] + [JsonPropertyName("description")] public string Description { get; set; } /// /// The URL to the package's icon. /// - [JsonProperty("iconUrl")] + [JsonPropertyName("iconUrl")] public string IconUrl { get; set; } /// @@ -61,110 +61,110 @@ public class PackageDetailsCatalogLeaf : CatalogLeaf /// Note that the NuGet.org catalog had this wrong in some cases. /// Example: https://api.nuget.org/v3/catalog0/data/2016.03.11.21.02.55/mvid.fody.2.json /// - [JsonProperty("isPrerelease")] + [JsonPropertyName("isPrerelease")] public bool IsPrerelease { get; set; } /// /// The package's language. /// - [JsonProperty("language")] + [JsonPropertyName("language")] public string Language { get; set; } /// /// THe URL to the package's license. /// - [JsonProperty("licenseUrl")] + [JsonPropertyName("licenseUrl")] public string LicenseUrl { get; set; } /// /// Whether the pacakge is listed. /// - [JsonProperty("listed")] + [JsonPropertyName("listed")] public bool? Listed { get; set; } /// /// The minimum NuGet client version needed to use this package. /// - [JsonProperty("minClientVersion")] + [JsonPropertyName("minClientVersion")] public string MinClientVersion { get; set; } /// /// The hash of the package encoded using Base64. /// Hash algorithm can be detected using . /// - [JsonProperty("packageHash")] + [JsonPropertyName("packageHash")] public string PackageHash { get; set; } /// /// The algorithm used to hash . /// - [JsonProperty("packageHashAlgorithm")] + [JsonPropertyName("packageHashAlgorithm")] public string PackageHashAlgorithm { get; set; } /// /// The size of the package .nupkg in bytes. /// - [JsonProperty("packageSize")] + [JsonPropertyName("packageSize")] public long PackageSize { get; set; } /// /// The URL for the package's home page. /// - [JsonProperty("projectUrl")] + [JsonPropertyName("projectUrl")] public string ProjectUrl { get; set; } /// /// The package's release notes. /// - [JsonProperty("releaseNotes")] + [JsonPropertyName("releaseNotes")] public string ReleaseNotes { get; set; } /// /// If true, the package requires its license to be accepted. /// - [JsonProperty("requireLicenseAcceptance")] + [JsonPropertyName("requireLicenseAcceptance")] public bool? RequireLicenseAcceptance { get; set; } /// /// The package's summary. /// - [JsonProperty("summary")] + [JsonPropertyName("summary")] public string Summary { get; set; } /// /// The package's tags. /// - [JsonProperty("tags")] + [JsonPropertyName("tags")] public List Tags { get; set; } /// /// The package's title. /// - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } /// /// The version string as it's originally found in the .nuspec. /// - [JsonProperty("verbatimVersion")] + [JsonPropertyName("verbatimVersion")] public string VerbatimVersion { get; set; } /// /// The package's License Expression. /// - [JsonProperty("licenseExpression")] + [JsonPropertyName("licenseExpression")] public string LicenseExpression { get; set; } /// /// The package's license file. /// - [JsonProperty("licenseFile")] + [JsonPropertyName("licenseFile")] public string LicenseFile { get; set; } /// /// The package's icon file. /// - [JsonProperty("iconFile")] + [JsonPropertyName("iconFile")] public string IconFile { get; set; } } } diff --git a/src/BaGet.Protocol/Models/PackageMetadata.cs b/src/BaGet.Protocol/Models/PackageMetadata.cs index 044e3f51a..2e9563d17 100644 --- a/src/BaGet.Protocol/Models/PackageMetadata.cs +++ b/src/BaGet.Protocol/Models/PackageMetadata.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -14,116 +14,116 @@ public class PackageMetadata /// /// The URL to the document used to produce this object. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The ID of the package. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string PackageId { get; set; } /// /// The full NuGet version after normalization, including any SemVer 2.0.0 build metadata. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The package's authors. /// - [JsonProperty("authors")] + [JsonPropertyName("authors")] public string Authors { get; set; } /// /// The dependencies of the package, grouped by target framework. /// - [JsonProperty("dependencyGroups")] + [JsonPropertyName("dependencyGroups")] public IReadOnlyList DependencyGroups { get; set; } /// /// The deprecation associated with the package, if any. /// - [JsonProperty("deprecation")] + [JsonPropertyName("deprecation")] public PackageDeprecation Deprecation { get; set; } /// /// The package's description. /// - [JsonProperty("description")] + [JsonPropertyName("description")] public string Description { get; set; } /// /// The URL to the package's icon. /// - [JsonProperty("iconUrl")] + [JsonPropertyName("iconUrl")] public string IconUrl { get; set; } /// /// The package's language. /// - [JsonProperty("language")] + [JsonPropertyName("language")] public string Language { get; set; } /// /// The URL to the package's license. /// - [JsonProperty("licenseUrl")] + [JsonPropertyName("licenseUrl")] public string LicenseUrl { get; set; } /// /// Whether the package is listed in search results. /// If , the package should be considered as listed. /// - [JsonProperty("listed")] + [JsonPropertyName("listed")] public bool? Listed { get; set; } /// /// The minimum NuGet client version needed to use this package. /// - [JsonProperty("minClientVersion")] + [JsonPropertyName("minClientVersion")] public string MinClientVersion { get; set; } /// /// The URL to download the package's content. /// - [JsonProperty("packageContent")] + [JsonPropertyName("packageContent")] public string PackageContentUrl { get; set; } /// /// The URL for the package's home page. /// - [JsonProperty("projectUrl")] + [JsonPropertyName("projectUrl")] public string ProjectUrl { get; set; } /// /// The package's publish date. /// - [JsonProperty("published")] + [JsonPropertyName("published")] public DateTimeOffset Published { get; set; } /// /// If true, the package requires its license to be accepted. /// - [JsonProperty("requireLicenseAcceptance")] + [JsonPropertyName("requireLicenseAcceptance")] public bool RequireLicenseAcceptance { get; set; } /// /// The package's summary. /// - [JsonProperty("summary")] + [JsonPropertyName("summary")] public string Summary { get; set; } /// /// The package's tags. /// - [JsonProperty("tags")] + [JsonPropertyName("tags")] public IReadOnlyList Tags { get; set; } /// /// The package's title. /// - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } } } diff --git a/src/BaGet.Protocol/Models/PackageVersionsResponse.cs b/src/BaGet.Protocol/Models/PackageVersionsResponse.cs index e42e8d3bb..4a8a64f93 100644 --- a/src/BaGet.Protocol/Models/PackageVersionsResponse.cs +++ b/src/BaGet.Protocol/Models/PackageVersionsResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,7 +13,7 @@ public class PackageVersionsResponse /// /// The versions, lowercased and normalized. /// - [JsonProperty("versions")] + [JsonPropertyName("versions")] public IReadOnlyList Versions { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationIndexPage.cs b/src/BaGet.Protocol/Models/RegistrationIndexPage.cs index 0f3679670..e7190af5c 100644 --- a/src/BaGet.Protocol/Models/RegistrationIndexPage.cs +++ b/src/BaGet.Protocol/Models/RegistrationIndexPage.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,34 +13,34 @@ public class RegistrationIndexPage /// /// The URL to the registration page. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationPageUrl { get; set; } /// /// The number of registration leafs in the page. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// if this package's registration is paged. The items can be found /// by following the page's . /// - [JsonProperty("items")] + [JsonPropertyName("items")] public IReadOnlyList ItemsOrNull { get; set; } /// /// This page's lowest package version. The version should be lowercased, normalized, /// and the SemVer 2.0.0 build metadata removed, if any. /// - [JsonProperty("lower")] + [JsonPropertyName("lower")] public string Lower { get; set; } /// /// This page's highest package version. The version should be lowercased, normalized, /// and the SemVer 2.0.0 build metadata removed, if any. /// - [JsonProperty("upper")] + [JsonPropertyName("upper")] public string Upper { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs b/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs index afca418b0..91668366a 100644 --- a/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs +++ b/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -12,19 +12,19 @@ public class RegistrationIndexPageItem /// /// The URL to the registration leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationLeafUrl { get; set; } /// /// The catalog entry containing the package metadata. /// - [JsonProperty("catalogEntry")] + [JsonPropertyName("catalogEntry")] public PackageMetadata PackageMetadata { get; set; } /// /// The URL to the package content (.nupkg) /// - [JsonProperty("packageContent")] + [JsonPropertyName("packageContent")] public string PackageContentUrl { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs b/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs index a4dbc24ce..79cf01bac 100644 --- a/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs +++ b/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -20,26 +20,26 @@ public class RegistrationIndexResponse /// /// The URL to the registration index. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationIndexUrl { get; set; } /// /// The registration index's type. /// - [JsonProperty("@type")] + [JsonPropertyName("@type")] public IReadOnlyList Type { get; set; } /// - /// The number of registration pages. See . + /// The number of registration pages. See . /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// The pages that contain all of the versions of the package, ordered /// by the package's version. /// - [JsonProperty("items")] + [JsonPropertyName("items")] public IReadOnlyList Pages { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs b/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs index 33718ac0f..c95ca7ed1 100644 --- a/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs +++ b/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -20,38 +20,38 @@ public class RegistrationLeafResponse /// /// The URL to the registration leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationLeafUrl { get; set; } /// /// The registration leaf's type. /// - [JsonProperty("@type")] + [JsonPropertyName("@type")] public IReadOnlyList Type { get; set; } /// /// Whether the package is listed. /// - [JsonProperty("listed")] + [JsonPropertyName("listed")] public bool Listed { get; set; } /// /// The URL to the package content (.nupkg). /// - [JsonProperty("packageContent")] + [JsonPropertyName("packageContent")] public string PackageContentUrl { get; set; } /// /// The date the package was published. On NuGet.org, /// is set to the year 1900 if the package is unlisted. /// - [JsonProperty("published")] + [JsonPropertyName("published")] public DateTimeOffset Published { get; set; } /// /// The URL to the package's registration index. /// - [JsonProperty("registration")] + [JsonPropertyName("registration")] public string RegistrationIndexUrl { get; set; } } } diff --git a/src/BaGet.Protocol/Models/ResponseAndResult.cs b/src/BaGet.Protocol/Models/ResponseAndResult.cs deleted file mode 100644 index cef4d392c..000000000 --- a/src/BaGet.Protocol/Models/ResponseAndResult.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using BaGet.Protocol.Models; - -namespace BaGet.Protocol -{ - internal class ResponseAndResult - { - public ResponseAndResult( - HttpMethod method, - string requestUri, - HttpStatusCode statusCode, - string reasonPhrase, - bool hasResult, - T result) - { - Method = method ?? throw new ArgumentNullException(nameof(method)); - RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri)); - StatusCode = statusCode; - ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase)); - HasResult = hasResult; - Result = result; - } - - public HttpMethod Method { get; } - public string RequestUri { get; } - public HttpStatusCode StatusCode { get; } - public string ReasonPhrase { get; } - public bool HasResult { get; } - public T Result { get; } - - public T GetResultOrThrow() - { - if (!HasResult) - { - throw new ProtocolException( - $"The HTTP request failed.{Environment.NewLine}" + - $"Request: {Method} {RequestUri}{Environment.NewLine}" + - $"Response: {(int)StatusCode} {ReasonPhrase}", - Method, - RequestUri, - StatusCode, - ReasonPhrase); - } - - return Result; - } - } -} diff --git a/src/BaGet.Protocol/Models/SearchContext.cs b/src/BaGet.Protocol/Models/SearchContext.cs index 7c63a03cf..6abfb92ea 100644 --- a/src/BaGet.Protocol/Models/SearchContext.cs +++ b/src/BaGet.Protocol/Models/SearchContext.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,10 +13,10 @@ public static SearchContext Default(string registrationBaseUrl) }; } - [JsonProperty("@vocab")] + [JsonPropertyName("@vocab")] public string Vocab { get; set; } - [JsonProperty("@base")] + [JsonPropertyName("@base")] public string Base { get; set; } } } diff --git a/src/BaGet.Protocol/Models/SearchResponse.cs b/src/BaGet.Protocol/Models/SearchResponse.cs index 50c2d215b..70fce7393 100644 --- a/src/BaGet.Protocol/Models/SearchResponse.cs +++ b/src/BaGet.Protocol/Models/SearchResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -10,19 +10,19 @@ namespace BaGet.Protocol.Models /// public class SearchResponse { - [JsonProperty("@context")] + [JsonPropertyName("@context")] public SearchContext Context { get; set; } /// /// The total number of matches, disregarding skip and take. /// - [JsonProperty("totalHits")] + [JsonPropertyName("totalHits")] public long TotalHits { get; set; } /// /// The packages that matched the search query. /// - [JsonProperty("data")] + [JsonPropertyName("data")] public IReadOnlyList Data { get; set; } } } diff --git a/src/BaGet.Protocol/Models/SearchResult.cs b/src/BaGet.Protocol/Models/SearchResult.cs index 365736680..4b0bb3681 100644 --- a/src/BaGet.Protocol/Models/SearchResult.cs +++ b/src/BaGet.Protocol/Models/SearchResult.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using BaGet.Protocol.Internal; -using Newtonsoft.Json; namespace BaGet.Protocol.Models { @@ -14,81 +14,81 @@ public class SearchResult /// /// The ID of the matched package. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string PackageId { get; set; } /// /// The latest version of the matched pacakge. This is the full NuGet version after normalization, /// including any SemVer 2.0.0 build metadata. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The description of the matched package. /// - [JsonProperty("description")] + [JsonPropertyName("description")] public string Description { get; set; } /// /// The authors of the matched package. /// - [JsonProperty("authors")] - [JsonConverter(typeof(SingleOrListConverter))] + [JsonPropertyName("authors")] + [JsonConverter(typeof(StringOrStringArrayJsonConverter))] public IReadOnlyList Authors { get; set; } /// /// The URL of the matched package's icon. /// - [JsonProperty("iconUrl")] + [JsonPropertyName("iconUrl")] public string IconUrl { get; set; } /// /// The URL of the matched package's license. /// - [JsonProperty("licenseUrl")] + [JsonPropertyName("licenseUrl")] public string LicenseUrl { get; set; } /// /// The URL of the matched package's homepage. /// - [JsonProperty("projectUrl")] + [JsonPropertyName("projectUrl")] public string ProjectUrl { get; set; } /// /// The URL for the matched package's registration index. /// - [JsonProperty("registration")] + [JsonPropertyName("registration")] public string RegistrationIndexUrl { get; set; } /// /// The summary of the matched package. /// - [JsonProperty("summary")] + [JsonPropertyName("summary")] public string Summary { get; set; } /// /// The tags of the matched package. /// - [JsonProperty("tags")] + [JsonPropertyName("tags")] public IReadOnlyList Tags { get; set; } /// /// The title of the matched package. /// - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } /// /// The total downloads for all versions of the matched package. /// - [JsonProperty("totalDownloads")] + [JsonPropertyName("totalDownloads")] public long TotalDownloads { get; set; } /// /// The versions of the matched package. /// - [JsonProperty("versions")] + [JsonPropertyName("versions")] public IReadOnlyList Versions { get; set; } } } diff --git a/src/BaGet.Protocol/Models/SearchResultVersion.cs b/src/BaGet.Protocol/Models/SearchResultVersion.cs index b7f6823f1..a3a2cb237 100644 --- a/src/BaGet.Protocol/Models/SearchResultVersion.cs +++ b/src/BaGet.Protocol/Models/SearchResultVersion.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -12,19 +12,19 @@ public class SearchResultVersion /// /// The registration leaf URL for this single version of the matched package. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationLeafUrl { get; set; } /// /// The package's full NuGet version after normalization, including any SemVer 2.0.0 build metadata. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The downloads for this single version of the matched package. /// - [JsonProperty("downloads")] + [JsonPropertyName("downloads")] public long Downloads { get; set; } } } diff --git a/src/BaGet.Protocol/Models/ServiceIndexItem.cs b/src/BaGet.Protocol/Models/ServiceIndexItem.cs index 76718489e..36ba43881 100644 --- a/src/BaGet.Protocol/Models/ServiceIndexItem.cs +++ b/src/BaGet.Protocol/Models/ServiceIndexItem.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -12,19 +12,19 @@ public class ServiceIndexItem /// /// The resource's base URL. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string ResourceUrl { get; set; } /// /// The resource's type. /// - [JsonProperty("@type")] + [JsonPropertyName("@type")] public string Type { get; set; } /// /// Human readable comments about the resource. /// - [JsonProperty("comment")] + [JsonPropertyName("comment")] public string Comment { get; set; } } } diff --git a/src/BaGet.Protocol/Models/ServiceIndexResponse.cs b/src/BaGet.Protocol/Models/ServiceIndexResponse.cs index 5988a8f4c..4b0327124 100644 --- a/src/BaGet.Protocol/Models/ServiceIndexResponse.cs +++ b/src/BaGet.Protocol/Models/ServiceIndexResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,13 +13,13 @@ public class ServiceIndexResponse /// /// The service index's version. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The resources declared by this service index. /// - [JsonProperty("resources")] + [JsonPropertyName("resources")] public IReadOnlyList Resources { get; set; } } } diff --git a/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs b/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs index 87bf27b30..1d172844f 100644 --- a/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs +++ b/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs @@ -35,16 +35,9 @@ public async Task GetPackageVersionsOrNullAsync( CancellationToken cancellationToken = default) { var id = packageId.ToLowerInvariant(); - var url = $"{_packageContentUrl}/{id}/index.json"; - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonOrDefaultAsync(url, cancellationToken); } /// diff --git a/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs b/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs index 8d19023e6..75f7071f9 100644 --- a/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs +++ b/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs @@ -1,5 +1,4 @@ using System; -using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -32,14 +31,8 @@ public async Task GetRegistrationIndexOrNullAsync( CancellationToken cancellationToken = default) { var url = $"{_packageMetadataUrl}/{packageId.ToLowerInvariant()}/index.json"; - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonOrDefaultAsync(url, cancellationToken); } /// @@ -47,9 +40,7 @@ public async Task GetRegistrationPageAsync( string pageUrl, CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(pageUrl, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(pageUrl, cancellationToken); } /// @@ -57,14 +48,7 @@ public async Task GetRegistrationLeafAsync( string leafUrl, CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(leafUrl, cancellationToken); - - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(leafUrl, cancellationToken); } } } diff --git a/src/BaGet.Protocol/Search/RawAutocompleteClient.cs b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs index 20602a3d8..26f63be00 100644 --- a/src/BaGet.Protocol/Search/RawAutocompleteClient.cs +++ b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs @@ -44,9 +44,7 @@ public async Task AutocompleteAsync( includeSemVer2, "q"); - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(url, cancellationToken); } public async Task ListPackageVersionsAsync( @@ -64,9 +62,7 @@ public async Task ListPackageVersionsAsync( includeSemVer2, "id"); - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(url, cancellationToken); } } } diff --git a/src/BaGet.Protocol/Search/RawSearchClient.cs b/src/BaGet.Protocol/Search/RawSearchClient.cs index 4b721030e..914b08b41 100644 --- a/src/BaGet.Protocol/Search/RawSearchClient.cs +++ b/src/BaGet.Protocol/Search/RawSearchClient.cs @@ -39,9 +39,7 @@ public async Task SearchAsync( { var url = AddSearchQueryString(_searchUrl, query, skip, take, includePrerelease, includeSemVer2, "q"); - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(url, cancellationToken); } internal static string AddSearchQueryString( diff --git a/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs b/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs index 830770300..70d85669c 100644 --- a/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs +++ b/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs @@ -30,11 +30,9 @@ public RawServiceIndexClient(HttpClient httpClient, string serviceIndexUrl) /// public async Task GetAsync(CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync( + return await _httpClient.GetFromJsonAsync( _serviceIndexUrl, cancellationToken); - - return response.GetResultOrThrow(); } } } diff --git a/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx b/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx index a531e26ab..834bb4264 100644 --- a/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx +++ b/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx @@ -36,7 +36,7 @@ interface IPackage { licenseUrl: string; downloadUrl: string; repositoryUrl: string; - repositoryType: string; + repositoryType?: string; releaseNotes: string; totalDownloads: number; packageType: PackageType; diff --git a/src/BaGet.UI/src/DisplayPackage/Registration.tsx b/src/BaGet.UI/src/DisplayPackage/Registration.tsx index dea000bbc..fc7d60ee0 100644 --- a/src/BaGet.UI/src/DisplayPackage/Registration.tsx +++ b/src/BaGet.UI/src/DisplayPackage/Registration.tsx @@ -29,7 +29,7 @@ export interface ICatalogEntry { listed: boolean; packageTypes: string[]; repositoryUrl: string; - repositoryType: string; + repositoryType?: string; authors: string; tags: string[]; dependencyGroups: IDependencyGroup[]; diff --git a/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx b/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx index e64764a89..6d996553a 100644 --- a/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx +++ b/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx @@ -3,7 +3,7 @@ import './SourceRepository.css'; interface ISourceRepositoryProps { url: string; - type: string; + type?: string; } class SourceRepository extends React.Component { diff --git a/tests/BaGet.Core.Tests/Metadata/ModelTests.cs b/tests/BaGet.Core.Tests/Metadata/ModelTests.cs new file mode 100644 index 000000000..61d3fcf28 --- /dev/null +++ b/tests/BaGet.Core.Tests/Metadata/ModelTests.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using BaGet.Protocol.Models; +using Xunit; + +namespace BaGet.Core.Tests.Metadata +{ + public class ModelTests + { + /// + /// BaGet extends the NuGet protocol to add more functionality. + /// Since System.Text.Json does not support polymorphic serialization, + /// this was implemented by duplicating the protocol's models from the + /// "BaGet.Protocol" project. These tests ensure that the duplicates + /// stay in sync with the original protocol models. + /// + public static IEnumerable ExtendedModelsData() + { + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(RegistrationIndexResponse), + DerivedType = typeof(BaGetRegistrationIndexResponse), + + AddedProperties = new Dictionary + { + { "TotalDownloads", typeof(long) }, + }, + + ModifiedProperties = new Dictionary + { + { + "Pages", + ( + From: typeof(IReadOnlyList), + To: typeof(IReadOnlyList) + ) + }, + } + } + }; + + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(RegistrationIndexPage), + DerivedType = typeof(BaGetRegistrationIndexPage), + + ModifiedProperties = new Dictionary + { + { + "ItemsOrNull", + ( + From: typeof(IReadOnlyList), + To: typeof(IReadOnlyList) + ) + }, + } + } + }; + + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(RegistrationIndexPageItem), + DerivedType = typeof(BaGetRegistrationIndexPageItem), + + ModifiedProperties = new Dictionary + { + { + "PackageMetadata", ( From: typeof(PackageMetadata), To: typeof(BaGetPackageMetadata) ) + }, + } + } + }; + + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(PackageMetadata), + DerivedType = typeof(BaGetPackageMetadata), + + AddedProperties = new Dictionary + { + { "Downloads", typeof(long) }, + { "HasReadme", typeof(bool) }, + { "PackageTypes", typeof(IReadOnlyList) }, + { "ReleaseNotes", typeof(string) }, + { "RepositoryUrl", typeof(string) }, + { "RepositoryType", typeof(string) }, + } + } + }; + } + + [Theory] + [MemberData(nameof(ExtendedModelsData))] + public void ValidateExtendedModels(ExtendedModelData data) + { + IReadOnlyDictionary originalProperties = data + .OriginalType + .GetProperties() + .ToDictionary(p => p.Name, p => p); + IReadOnlyDictionary derivedProperties = data + .DerivedType + .GetProperties() + .ToDictionary(p => p.Name, p => p); + + // Check that all properties on the original model are present on the derived model. + var missingProperties = originalProperties.Keys.Where(name => !derivedProperties.ContainsKey(name)); + + Assert.True( + !missingProperties.Any(), + $"The following properties are missing from the derived type: {string.Join(',', missingProperties)}"); + + // Check that all properties on the derived model are as expected compared to the original model. + foreach (var derivedProperty in derivedProperties.Values) + { + // If the property was added, check that it is not on the original type. + if (data.AddedProperties.TryGetValue(derivedProperty.Name, out var addedType)) + { + Assert.True( + !originalProperties.ContainsKey(derivedProperty.Name), + $"Added property '{derivedProperty.Name}' exists on the original type {data.OriginalType}"); + Assert.True( + addedType == derivedProperty.PropertyType, + $"Added property '{derivedProperty.Name}' on type {data.DerivedType} has unexpected property type\n" + + $"Expected: {addedType}\n" + + $"Actual: {derivedProperty.PropertyType}"); + continue; + } + + // This property should exist on both the original and derived models. + // This property should have the same "JsonPropertyName" attribute values. + var originalProperty = Assert.Contains(derivedProperty.Name, originalProperties); + + var originalJsonName = GetAttributeArgs(originalProperty)?.FirstOrDefault(); + var derivedJsonName = GetAttributeArgs(derivedProperty)?.FirstOrDefault(); + + Assert.True( + originalJsonName != null, + $"Property '{originalProperty.Name}' on type '{data.OriginalType}' " + + "does not have a JsonPropertyName attribute"); + Assert.True( + derivedJsonName != null, + $"Property '{derivedProperty.Name}' on type '{data.DerivedType}' " + + "does not have a JsonPropertyName attribute"); + Assert.True( + originalJsonName.ToString() == derivedJsonName.ToString(), + $"Property '{derivedProperty.Name}' on type '{data.DerivedType}' " + + "has a different JsonPropertyName attribute value than " + + $"on type '{data.OriginalType}'.\nExpected: '{originalJsonName}'\n" + + $"Actual: '{derivedJsonName}'"); + + var originalJsonConverterArgs = GetAttributeArgs(originalProperty); + var derivedJsonConverterArgs = GetAttributeArgs(derivedProperty); + + // If the property was modified, check that the property types are expected. + if (data.ModifiedProperties.TryGetValue(derivedProperty.Name, out var modifiedTypes)) + { + Assert.True( + originalProperty.PropertyType == modifiedTypes.From, + $"Modified property '{originalProperty.Name}' on type {data.OriginalType} has unexpected property type\n" + + $"Expected: {modifiedTypes.From}\n" + + $"Actual: {originalProperty.PropertyType}"); + Assert.True( + derivedProperty.PropertyType == modifiedTypes.To, + $"Modified property '{derivedProperty.Name}' on type {data.DerivedType} has unexpected property type\n" + + $"Expected: {modifiedTypes.To}\n" + + $"Actual: {derivedProperty.PropertyType}"); + + if (originalJsonConverterArgs != null || derivedJsonConverterArgs != null) + { + throw new NotSupportedException( + "JSON converters on modified properties is not supported"); + } + + continue; + } + + // Otherwise, this property should be identical to the original property. + Assert.True( + originalProperty.PropertyType == derivedProperty.PropertyType, + $"Property '{derivedProperty.Name}' on type {data.DerivedType} has unexpected property type\n" + + $"Expected: {originalProperty.PropertyType}\n" + + $"Actual: {derivedProperty.PropertyType}"); + Assert.True( + originalJsonConverterArgs?.First() == derivedJsonConverterArgs?.First(), + $"Property '{derivedProperty.Name}' on type '{data.DerivedType}' " + + "has unexpected JsonConverter value.\n" + + $"Expected: '{originalJsonConverterArgs?.First()}'\n" + + $"Actual: '{derivedJsonConverterArgs?.First()}'"); + } + } + + private IList GetAttributeArgs(PropertyInfo property) + { + return property + .CustomAttributes + ?.SingleOrDefault(x => x.AttributeType == typeof(TAttribute)) + ?.ConstructorArguments; + } + + public class ExtendedModelData + { + /// + /// The model's type in the "BaGet.Protocol" project that was extended. + /// + public Type OriginalType { get; set; } + + /// + /// The model's type in the "BaGet.Core" project that extends a + /// type from the "BaGet.Protocol" project. + /// + public Type DerivedType { get; set; } + + /// + /// The properties added by the model type in the "BaGet.Core" project. + /// + public Dictionary AddedProperties { get; set; } = new Dictionary(); + + /// + /// The properties whose types were modified by the model type in the + /// "BaGet.Core" project. + /// + public Dictionary ModifiedProperties { get; set; } + = new Dictionary(); + } + } +} diff --git a/tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs b/tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs new file mode 100644 index 000000000..5229bd798 --- /dev/null +++ b/tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs @@ -0,0 +1,87 @@ +using System.Text.Json; +using BaGet.Protocol.Internal; +using Xunit; + +namespace BaGet.Protocol.Tests +{ + public class PackageDependencyRangeJsonConverterTests + { + [Fact] + public void DeserializesNull() + { + var json = @"null"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + var result = JsonSerializer.Deserialize(json, options); + + Assert.Null(result); + } + + [Fact] + public void DeserializesString() + { + var json = @"""Hello"""; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + var result = JsonSerializer.Deserialize(json, options); + + Assert.Equal("Hello", result); + } + + [Fact] + public void DeserializesStringArray() + { + var json = @"[""first"", ""second"", ""third""]"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + var result = JsonSerializer.Deserialize(json, options); + + Assert.Equal("first", result); + } + + [Theory] + [InlineData(@"false")] + [InlineData(@"0")] + [InlineData(@"{")] + [InlineData(@"[")] + [InlineData(@"[""hello""")] + [InlineData(@"[""hello""}")] + [InlineData(@"[""hello"", 1")] + public void ThrowsOnInvalidJson(string json) + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + Assert.Throws( + () => JsonSerializer.Deserialize(json, options)); + } + + [Fact] + public void SerializesNull() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var json = JsonSerializer.Serialize(null, options); + + Assert.Equal("null", json); + } + + [Fact] + public void SerializesString() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var json = JsonSerializer.Serialize("foo", options); + + Assert.Equal(@"""foo""", json); + } + } +} diff --git a/tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs b/tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs new file mode 100644 index 000000000..9f4b77df2 --- /dev/null +++ b/tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Text.Json; +using BaGet.Protocol.Internal; +using Xunit; + +namespace BaGet.Protocol.Tests +{ + public class StringOrStringArrayJsonConverterTests + { + [Fact] + public void DeserializesEmptyString() + { + var json = @""""""; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + var first = Assert.Single(result); + Assert.NotNull(first); + Assert.True(string.IsNullOrEmpty(first)); + } + + [Fact] + public void DeserializesString() + { + var json = @"""Foo bar"""; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + var first = Assert.Single(result); + Assert.Equal("Foo bar", first); + } + + [Fact] + public void DeserializesEmptyArray() + { + var json = "[]"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + Assert.Empty(result); + } + + [Fact] + public void DeserializesArray() + { + var json = @"[""Foo"", ""bar""]"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + Assert.Equal(2, result.Count); + Assert.Equal("Foo", result[0]); + Assert.Equal("bar", result[1]); + } + + [Theory] + [InlineData(@"false")] + [InlineData(@"0")] + [InlineData(@"{")] + [InlineData(@"[")] + [InlineData(@"[""hello""")] + [InlineData(@"[""hello""}")] + [InlineData(@"[""hello"", 1")] + public void ThrowsOnInvalidJson(string json) + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + Assert.Throws( + () => JsonSerializer.Deserialize>(json, options)); + } + + [Fact] + public void SerializesNull() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + IReadOnlyList list = null; + + var json = JsonSerializer.Serialize(list, options); + + Assert.Equal("null", json); + } + + [Fact] + public void SerializesEmptyString() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + IReadOnlyList list = new List { "" }; + + var json = JsonSerializer.Serialize(list, options); + + Assert.Equal(@"[""""]", json); + } + + [Fact] + public void SerializesList() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + IReadOnlyList list = new List { "Hello", "World", null }; + + var json = JsonSerializer.Serialize(list, options); + + Assert.Equal(@"[""Hello"",""World"",null]", json); + } + } +} diff --git a/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs b/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs index 5ebb2ccf8..55ecc029a 100644 --- a/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs +++ b/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs @@ -45,7 +45,8 @@ public async Task GetPackageDetailsLeaf() var leaf = await _target.GetPackageDetailsLeafAsync(TestData.PackageDetailsCatalogLeafUrl); Assert.Equal(TestData.PackageDetailsCatalogLeafUrl, leaf.CatalogLeafUrl); - Assert.Equal(CatalogLeafType.PackageDetails, leaf.Type); + Assert.Equal("PackageDetails", leaf.Type[0]); + Assert.Equal("catalog:Permalink", leaf.Type[1]); Assert.Equal("Test.Package", leaf.PackageId); Assert.Equal("1.0.0", leaf.PackageVersion); @@ -57,7 +58,8 @@ public async Task GetPackageDeleteLeaf() var leaf = await _target.GetPackageDeleteLeafAsync(TestData.PackageDeleteCatalogLeafUrl); Assert.Equal(TestData.PackageDeleteCatalogLeafUrl, leaf.CatalogLeafUrl); - Assert.Equal(CatalogLeafType.PackageDelete, leaf.Type); + Assert.Equal("PackageDelete", leaf.Type[0]); + Assert.Equal("catalog:Permalink", leaf.Type[1]); Assert.Equal("Deleted.Package", leaf.PackageId); Assert.Equal("1.0.0", leaf.PackageVersion); diff --git a/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs b/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs index 735649f25..0705c10c5 100644 --- a/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs +++ b/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs @@ -55,7 +55,10 @@ private HttpResponseMessage Send(HttpRequestMessage request) { RequestMessage = request, StatusCode = HttpStatusCode.OK, - Content = new StringContent(getContent()), + Content = new StringContent( + getContent(), + encoding: null, + mediaType: "application/json"), }; } } diff --git a/tests/BaGet.Tests/ApiIntegrationTests.cs b/tests/BaGet.Tests/ApiIntegrationTests.cs index 19850ac56..780a0deef 100644 --- a/tests/BaGet.Tests/ApiIntegrationTests.cs +++ b/tests/BaGet.Tests/ApiIntegrationTests.cs @@ -99,7 +99,7 @@ public async Task AutocompleteReturnsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""context"": { + ""@context"": { ""@vocab"": ""http://schema.nuget.org/schema#"" }, ""totalHits"": 1, @@ -118,7 +118,7 @@ public async Task AutocompleteReturnsEmpty() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""context"": { + ""@context"": { ""@vocab"": ""http://schema.nuget.org/schema#"" }, ""totalHits"": 0, @@ -188,7 +188,6 @@ public async Task PackageMetadataReturnsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""totalDownloads"": 0, ""@id"": ""http://localhost/v3/registration/defaultpackage/index.json"", ""@type"": [ ""catalog:CatalogRoot"", @@ -200,9 +199,12 @@ public async Task PackageMetadataReturnsOk() { ""@id"": ""http://localhost/v3/registration/defaultpackage/index.json"", ""count"": 1, + ""lower"": ""1.2.3"", + ""upper"": ""1.2.3"", ""items"": [ { ""@id"": ""http://localhost/v3/registration/defaultpackage/1.2.3.json"", + ""packageContent"": ""http://localhost/v3/package/defaultpackage/1.2.3/defaultpackage.1.2.3.nupkg"", ""catalogEntry"": { ""downloads"": 0, ""hasReadme"": false, @@ -211,13 +213,10 @@ public async Task PackageMetadataReturnsOk() ], ""releaseNotes"": """", ""repositoryUrl"": """", - ""repositoryType"": null, - ""@id"": null, ""id"": ""DefaultPackage"", ""version"": ""1.2.3"", ""authors"": ""Default package author"", ""dependencyGroups"": [], - ""deprecation"": null, ""description"": ""Default package description"", ""iconUrl"": """", ""language"": """", @@ -231,14 +230,12 @@ public async Task PackageMetadataReturnsOk() ""summary"": """", ""tags"": [], ""title"": """" - }, - ""packageContent"": ""http://localhost/v3/package/defaultpackage/1.2.3/defaultpackage.1.2.3.nupkg"" + } } - ], - ""lower"": ""1.2.3"", - ""upper"": ""1.2.3"" + ] } - ] + ], + ""totalDownloads"": 0 }", json); } @@ -259,7 +256,6 @@ public async Task PackageMetadataLeafReturnsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""downloads"": 0, ""@id"": ""http://localhost/v3/registration/defaultpackage/1.2.3.json"", ""@type"": [ ""Package"", diff --git a/tests/BaGet.Tests/TestData.resx b/tests/BaGet.Tests/TestData.resx index e2b63e4f3..e804f8787 100644 --- a/tests/BaGet.Tests/TestData.resx +++ b/tests/BaGet.Tests/TestData.resx @@ -1,17 +1,17 @@  - @@ -118,6 +118,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - {"version":"3.0.0","resources":[{"@id":"http://localhost/api/v2/package","@type":"PackagePublish/2.0.0","comment":null},{"@id":"http://localhost/api/v2/symbol","@type":"SymbolPackagePublish/4.9.0","comment":null},{"@id":"http://localhost/v3/search","@type":"SearchQueryService","comment":null},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-beta","comment":null},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-rc","comment":null},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl","comment":null},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-rc","comment":null},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-beta","comment":null},{"@id":"http://localhost/v3/package","@type":"PackageBaseAddress/3.0.0","comment":null},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService","comment":null},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-rc","comment":null},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-beta","comment":null}]} + {"version":"3.0.0","resources":[{"@id":"http://localhost/api/v2/package","@type":"PackagePublish/2.0.0"},{"@id":"http://localhost/api/v2/symbol","@type":"SymbolPackagePublish/4.9.0"},{"@id":"http://localhost/v3/search","@type":"SearchQueryService"},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-beta"},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-rc"},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl"},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-rc"},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-beta"},{"@id":"http://localhost/v3/package","@type":"PackageBaseAddress/3.0.0"},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService"},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-rc"},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-beta"}]} - \ No newline at end of file +