From 5df5ff2f35987af59fd76dbff3dfc267cdfb8ffc Mon Sep 17 00:00:00 2001 From: bdovaz Date: Tue, 10 Dec 2024 22:56:52 +0100 Subject: [PATCH] Several improvements - Partial AOT support - Change from MVC to Minimal API - Change from Newtonsoft.Json to System.Text.Json - Ability to configure the packages filter - Add REST API tests --- Dockerfile | 5 +- examples/docker/docker-compose.yml | 9 +- examples/docker/registry.json | 4 + src/Directory.Packages.props | 2 +- .../ApiControllerTests.cs | 201 ++++++++++++++++++ .../UnityNuGet.Server.Tests.csproj | 24 +++ .../UnityNuGetWebApplicationFactory.cs | 34 +++ .../Controllers/ApiController.cs | 124 ----------- .../EndpointRouteBuilderExtensions.cs | 133 +++++++++++- src/UnityNuGet.Server/Program.cs | 93 ++++---- .../Properties/launchSettings.json | 54 ++--- src/UnityNuGet.Server/RegistryCacheUpdater.cs | 11 +- .../UnityNuGet.Server.csproj | 1 - src/UnityNuGet.Tests/NativeTests.cs | 26 +-- ...NpmPackageListAllResponseConverterTests.cs | 40 ++++ src/UnityNuGet.Tests/RegistryCacheTests.cs | 4 +- .../VersionRangeConverterTests.cs | 40 ++++ src/UnityNuGet.sln | 110 +++++----- src/UnityNuGet/JsonCommonExtensions.cs | 67 ++---- src/UnityNuGet/Npm/NpmDistribution.cs | 34 +-- src/UnityNuGet/Npm/NpmError.cs | 9 +- src/UnityNuGet/Npm/NpmObject.cs | 19 +- src/UnityNuGet/Npm/NpmPackage.cs | 41 ++-- src/UnityNuGet/Npm/NpmPackageCacheEntry.cs | 40 ++-- src/UnityNuGet/Npm/NpmPackageInfo.cs | 29 +-- .../Npm/NpmPackageListAllResponse.cs | 31 +-- src/UnityNuGet/Npm/NpmPackageVersion.cs | 37 ++-- src/UnityNuGet/Npm/NpmSourceRepository.cs | 42 ++-- .../NpmPackageListAllResponseConverter.cs | 50 +++++ src/UnityNuGet/Registry.cs | 8 +- src/UnityNuGet/RegistryCache.cs | 19 +- src/UnityNuGet/RegistryEntry.cs | 12 +- src/UnityNuGet/RegistryOptions.cs | 4 +- src/UnityNuGet/UnityAsmdef.cs | 28 +-- .../UnityNugetJsonSerializerContext.cs | 19 ++ src/UnityNuGet/UnityPackage.cs | 28 +-- src/UnityNuGet/VersionRangeConverter.cs | 24 +++ 37 files changed, 920 insertions(+), 536 deletions(-) create mode 100644 src/UnityNuGet.Server.Tests/ApiControllerTests.cs create mode 100644 src/UnityNuGet.Server.Tests/UnityNuGet.Server.Tests.csproj create mode 100644 src/UnityNuGet.Server.Tests/UnityNuGetWebApplicationFactory.cs delete mode 100644 src/UnityNuGet.Server/Controllers/ApiController.cs create mode 100644 src/UnityNuGet.Tests/NpmPackageListAllResponseConverterTests.cs create mode 100644 src/UnityNuGet.Tests/VersionRangeConverterTests.cs create mode 100644 src/UnityNuGet/NpmPackageListAllResponseConverter.cs create mode 100644 src/UnityNuGet/UnityNugetJsonSerializerContext.cs create mode 100644 src/UnityNuGet/VersionRangeConverter.cs diff --git a/Dockerfile b/Dockerfile index 42adbc71..2ec987aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG TARGETARCH WORKDIR /app -RUN mkdir -p src/UnityNuGet && mkdir -p src/UnityNuGet.Server && mkdir -p src/UnityNuGet.Tests +RUN mkdir -p src/UnityNuGet && mkdir -p src/UnityNuGet.Server && mkdir -p src/UnityNuGet.Tests && mkdir -p src/UnityNuGet.Server.Tests COPY src/Directory.Build.props src/Directory.Build.props COPY src/Directory.Packages.props src/Directory.Packages.props @@ -10,10 +10,11 @@ COPY src/*.sln src COPY src/UnityNuGet/*.csproj src/UnityNuGet COPY src/UnityNuGet.Server/*.csproj src/UnityNuGet.Server COPY src/UnityNuGet.Tests/*.csproj src/UnityNuGet.Tests +COPY src/UnityNuGet.Server.Tests/*.csproj src/UnityNuGet.Server.Tests RUN dotnet restore src -a $TARGETARCH COPY . ./ -RUN dotnet publish src -a $TARGETARCH -c Release -o /app/src/out +RUN dotnet publish src/UnityNuGet.Server -a $TARGETARCH -c Release -o /app/src/out # Build runtime image FROM mcr.microsoft.com/dotnet/aspnet:9.0 diff --git a/examples/docker/docker-compose.yml b/examples/docker/docker-compose.yml index 7cf2e6e0..ce841243 100644 --- a/examples/docker/docker-compose.yml +++ b/examples/docker/docker-compose.yml @@ -4,16 +4,17 @@ services: build: ../.. environment: - Registry:RootHttpUrl=http://localhost:5000/ # Server Url to build the absolute path to the package. + - Registry:Filter= # Filter in regex format so that only the indicated packages are processed. - Registry:UnityScope=org.custom # Packages prefix, default is "org.nuget" but it can be modified to be able to have several containers with different prefixes and to be able to add several scope registries. - Registry:MinimumUnityVersion=2020.1 # Minimum version of Unity required to install packages, default is "2019.1". - Registry:PackageNameNuGetPostFix= (Custom NuGet) # Suffix of the package title, useful in case of having several containers and several scope registries, default is " (NuGet)". - - Registry:RegistryFilePath=custom_registry.json # Path to the file (relative or absolute) where the packages registry file will be stored, default is "registry.json". - - Registry:RootPersistentFolder=custom_unity_packages # Path to the folder (relative or absolute) where the packages cache will be stored, default is "unity_packages". + - Registry:RegistryFilePath=/data/registry.json # Path to the file (relative or absolute) where the packages registry file will be stored, default is "registry.json". + - Registry:RootPersistentFolder=/data/unity_packages # Path to the folder (relative or absolute) where the packages cache will be stored, default is "unity_packages". - Registry:UpdateInterval=00:01:00 # Packages update interval, default is "00:10:00" (10 minutes). - Logging:LogLevel:Default=Information ports: - 5000:8080 volumes: - - ./registry.json:/app/custom_registry.json # Override the package registry to be able to add or remove packages. - - ./unity_packages:/app/custom_unity_packages # Map the folder with the packages cache. + - ./registry.json:/data/registry.json # Override the package registry to be able to add or remove packages. + - ./unity_packages:/data/unity_packages # Map the folder with the packages cache. - ./NuGet.Config:/root/.nuget/NuGet/NuGet.Config # Override Nuget.config file with repository information. This file can be used to configure a custom NuGet repository: https://docs.microsoft.com/en-us/nuget/reference/nuget-config-file diff --git a/examples/docker/registry.json b/examples/docker/registry.json index b0227b0b..89755093 100644 --- a/examples/docker/registry.json +++ b/examples/docker/registry.json @@ -22,6 +22,10 @@ "listed": true, "version": "4.4.0" }, + "System.Linq.Async": { + "listed": true, + "version": "4.0.0" + }, "System.Memory": { "listed": true, "version": "4.5.0" diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 70b474c7..b88276b5 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,7 +4,7 @@ - + diff --git a/src/UnityNuGet.Server.Tests/ApiControllerTests.cs b/src/UnityNuGet.Server.Tests/ApiControllerTests.cs new file mode 100644 index 00000000..13fa4556 --- /dev/null +++ b/src/UnityNuGet.Server.Tests/ApiControllerTests.cs @@ -0,0 +1,201 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using UnityNuGet.Npm; + +namespace UnityNuGet.Server.Tests +{ + public class ApiControllerTests + { + private readonly UnityNuGetWebApplicationFactory _webApplicationFactory; + + public ApiControllerTests() + { + _webApplicationFactory = new UnityNuGetWebApplicationFactory(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _webApplicationFactory.Dispose(); + } + + [Test] + public async Task Home_Success() + { + using HttpClient client = _webApplicationFactory.CreateDefaultClient(); + + HttpResponseMessage response = await client.GetAsync("/"); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Found)); + Assert.That(response.Headers.Location, Is.EqualTo(new Uri("/-/all", UriKind.Relative))); + + string responseContent = await response.Content.ReadAsStringAsync(); + + Assert.That(responseContent, Is.Empty); + } + + [Test] + public async Task GetAll_Success() + { + using HttpClient httpClient = _webApplicationFactory.CreateDefaultClient(); + + await WaitForInitialization(_webApplicationFactory.Services); + + HttpResponseMessage response = await httpClient.GetAsync("/-/all"); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string responseContent = await response.Content.ReadAsStringAsync(); + + NpmPackageListAllResponse npmPackageListAllResponse = JsonSerializer.Deserialize(responseContent, UnityNugetJsonSerializerContext.Default.NpmPackageListAllResponse)!; + + Assert.That(npmPackageListAllResponse.Packages, Has.Count.EqualTo(1)); + + Assert.Multiple(() => + { + string packageName = $"org.nuget.{UnityNuGetWebApplicationFactory.PackageName.ToLowerInvariant()}"; + + Assert.That(npmPackageListAllResponse.Packages.ContainsKey(packageName), Is.True); + Assert.That(npmPackageListAllResponse.Packages[packageName].Name, Is.EqualTo(packageName)); + Assert.That(npmPackageListAllResponse.Packages[packageName].Description, Is.Not.Null); + Assert.That(npmPackageListAllResponse.Packages[packageName].Author, Is.Not.Null); + }); + } + + [Test] + public async Task GetPackage_NotFound() + { + using HttpClient httpClient = _webApplicationFactory.CreateDefaultClient(); + + await WaitForInitialization(_webApplicationFactory.Services); + + HttpResponseMessage response = await httpClient.GetAsync($"/InvalidPackageName"); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string responseContent = await response.Content.ReadAsStringAsync(); + + NpmError npmError = JsonSerializer.Deserialize(responseContent, UnityNugetJsonSerializerContext.Default.NpmError)!; + + Assert.That(npmError.Error, Is.EqualTo("not_found")); + } + + [Test] + public async Task GetPackage_Success() + { + using HttpClient httpClient = _webApplicationFactory.CreateDefaultClient(); + + await WaitForInitialization(_webApplicationFactory.Services); + + string packageName = $"org.nuget.{UnityNuGetWebApplicationFactory.PackageName.ToLowerInvariant()}"; + + HttpResponseMessage response = await httpClient.GetAsync($"/{packageName}"); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string responseContent = await response.Content.ReadAsStringAsync(); + + NpmPackage npmPackage = JsonSerializer.Deserialize(responseContent, UnityNugetJsonSerializerContext.Default.NpmPackage)!; + + Assert.Multiple(() => + { + Assert.That(npmPackage.Id, Is.EqualTo(packageName)); + Assert.That(npmPackage.Revision, Is.Not.Null); + Assert.That(npmPackage.Name, Is.EqualTo(packageName)); + Assert.That(npmPackage.License, Is.Not.Null); + Assert.That(npmPackage.Description, Is.Not.Null); + }); + } + + [Test] + [TestCase("org.nuget.newtonsoft.json", "InvalidFile")] + [TestCase("InvalidId", "org.nuget.newtonsoft.json-11.0.1.tgz")] + [TestCase("org.nuget.newtonsoft.json", "org.nuget.newtonsoft.json_11.0.1.tgz")] + [TestCase("org.nuget.newtonsoft.json", "org.nuget.newtonsoft.json-11.0.1.InvalidExtension")] + public async Task DownloadPackage_NotFound(string id, string file) + { + using HttpClient httpClient = _webApplicationFactory.CreateDefaultClient(); + + await WaitForInitialization(_webApplicationFactory.Services); + + HttpResponseMessage response = await httpClient.GetAsync($"/{id}/-/{file}"); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + string responseContent = await response.Content.ReadAsStringAsync(); + + NpmError npmError = JsonSerializer.Deserialize(responseContent, UnityNugetJsonSerializerContext.Default.NpmError)!; + + Assert.That(npmError.Error, Is.EqualTo("not_found")); + } + + [Test] + public async Task DownloadPackage_Head_Success() + { + using HttpClient httpClient = _webApplicationFactory.CreateDefaultClient(); + + await WaitForInitialization(_webApplicationFactory.Services); + + string packageName = $"org.nuget.{UnityNuGetWebApplicationFactory.PackageName.ToLowerInvariant()}"; + + HttpRequestMessage httpRequestMessage = new() + { + RequestUri = new Uri($"/{packageName}/-/{packageName}-11.0.1.tgz", UriKind.Relative), + Method = HttpMethod.Head + }; + + HttpResponseMessage response = await httpClient.SendAsync(httpRequestMessage); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + byte[] responseContent = await response.Content.ReadAsByteArrayAsync(); + + Assert.Multiple(() => + { + Assert.That(responseContent, Is.Empty); + + Assert.That(response.Content.Headers.ContentType!.MediaType, Is.EqualTo("application/octet-stream")); + Assert.That(response.Content.Headers.ContentLength, Is.GreaterThan(0)); + }); + } + + [Test] + public async Task DownloadPackage_Get_Success() + { + using HttpClient httpClient = _webApplicationFactory.CreateDefaultClient(); + + await WaitForInitialization(_webApplicationFactory.Services); + + string packageName = $"org.nuget.{UnityNuGetWebApplicationFactory.PackageName.ToLowerInvariant()}"; + + HttpResponseMessage response = await httpClient.GetAsync($"/{packageName}/-/{packageName}-11.0.1.tgz"); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + byte[] responseContent = await response.Content.ReadAsByteArrayAsync(); + + Assert.Multiple(() => + { + Assert.That(responseContent, Is.Not.Empty); + + Assert.That(response.Content.Headers.ContentType!.MediaType, Is.EqualTo("application/octet-stream")); + Assert.That(response.Content.Headers.ContentLength, Is.GreaterThan(0)); + }); + } + + private static async Task WaitForInitialization(IServiceProvider serviceProvider) + { + RegistryCacheSingleton registryCacheSingleton = serviceProvider.GetRequiredService(); + + while (registryCacheSingleton.Instance == null) + { + await Task.Delay(25); + } + } + } +} diff --git a/src/UnityNuGet.Server.Tests/UnityNuGet.Server.Tests.csproj b/src/UnityNuGet.Server.Tests/UnityNuGet.Server.Tests.csproj new file mode 100644 index 00000000..2485323e --- /dev/null +++ b/src/UnityNuGet.Server.Tests/UnityNuGet.Server.Tests.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + false + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + \ No newline at end of file diff --git a/src/UnityNuGet.Server.Tests/UnityNuGetWebApplicationFactory.cs b/src/UnityNuGet.Server.Tests/UnityNuGetWebApplicationFactory.cs new file mode 100644 index 00000000..0406a007 --- /dev/null +++ b/src/UnityNuGet.Server.Tests/UnityNuGetWebApplicationFactory.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace UnityNuGet.Server.Tests +{ + internal class UnityNuGetWebApplicationFactory : WebApplicationFactory + { + public const string PackageName = "Newtonsoft.Json"; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + + builder.ConfigureAppConfiguration(builder => + { + builder.AddInMemoryCollection(new Dictionary + { + { WebHostDefaults.ServerUrlsKey, "http://localhost" } + }); + }); + + builder.ConfigureServices(services => + { + services.Configure(options => + { + options.Filter = PackageName; + }); + }); + } + } +} diff --git a/src/UnityNuGet.Server/Controllers/ApiController.cs b/src/UnityNuGet.Server/Controllers/ApiController.cs deleted file mode 100644 index fd81cacb..00000000 --- a/src/UnityNuGet.Server/Controllers/ApiController.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.AspNetCore.Mvc; -using UnityNuGet.Npm; - -namespace UnityNuGet.Server.Controllers -{ - /// - /// Main entry to emulate the following NPM endpoints: - /// - /// - "-/all": query all packages (return json) - /// - "{packageId}": query a specific package (return json) - /// - "{package_id}/-/{package_file}": download a specific package - /// - [Route("")] - [ApiController] - public class ApiController(RegistryCacheSingleton cacheSingleton, RegistryCacheReport registryCacheReport) : ControllerBase - { - private readonly RegistryCacheSingleton _cacheSingleton = cacheSingleton; - private readonly RegistryCacheReport _registryCacheReport = registryCacheReport; - - // GET / - [HttpGet("")] - public IActionResult Home() - { - return Ok(); - } - - // GET -/all - [HttpGet("-/all")] - public JsonResult GetAll() - { - if (!TryGetInstance(out RegistryCache? instance, out NpmError? error)) return new JsonResult(error); - - NpmPackageListAllResponse? result = instance?.All(); - return new JsonResult(result); - } - - // GET {packageId} - [HttpGet("{id}")] - public JsonResult GetPackage(string id) - { - if (!TryGetInstance(out RegistryCache? instance, out NpmError? error)) return new JsonResult(error); - - NpmPackage? package = instance?.GetPackage(id); - if (package == null) - { - return new JsonResult(NpmError.NotFound); - } - - return new JsonResult(package); - } - - // GET {package_id}/-/{package_file} - [HttpGet("{id}/-/{file}")] - [HttpHead("{id}/-/{file}")] - public IActionResult DownloadPackage(string id, string file) - { - if (!TryGetInstance(out RegistryCache? instance, out NpmError? error)) return new JsonResult(error); - - NpmPackage? package = instance?.GetPackage(id); - if (package == null) - { - return new JsonResult(NpmError.NotFound); - } - - if (!file.StartsWith(id + "-") || !file.EndsWith(".tgz")) - { - return new JsonResult(NpmError.NotFound); - } - - string? filePath = instance?.GetPackageFilePath(file); - if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath)) - { - return new JsonResult(NpmError.NotFound); - } - - // This method can be called with HEAD request, so in that case we just calculate the content length - if (Request.Method.Equals("HEAD")) - { - Response.ContentType = "application/octet-stream"; - Response.ContentLength = new FileInfo(filePath).Length; - return Ok(); - } - else - { - return new PhysicalFileResult(filePath, "application/octet-stream") { FileDownloadName = file }; - } - } - - private bool TryGetInstance(out RegistryCache? cacheInstance, out NpmError? npmError) - { - RegistryCache? instance = _cacheSingleton.Instance; - cacheInstance = instance; - - if (instance == null) - { - if (_registryCacheReport.ErrorMessages.Any()) - { - var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine("Error initializing the server:"); - - foreach (string error in _registryCacheReport.ErrorMessages) - { - stringBuilder.AppendLine(error); - } - - npmError = new NpmError("not_initialized", stringBuilder.ToString()); - } - else - { - npmError = new NpmError("not_initialized", $"The server is initializing ({_registryCacheReport.Progress:F1}% completed). Please retry later..."); - } - } - else - { - npmError = null; - } - - return instance != null; - } - } -} diff --git a/src/UnityNuGet.Server/EndpointRouteBuilderExtensions.cs b/src/UnityNuGet.Server/EndpointRouteBuilderExtensions.cs index ee7d3660..0eaa04c3 100644 --- a/src/UnityNuGet.Server/EndpointRouteBuilderExtensions.cs +++ b/src/UnityNuGet.Server/EndpointRouteBuilderExtensions.cs @@ -1,16 +1,109 @@ -using Microsoft.AspNetCore.Http; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Scriban; using Scriban.Parsing; using Scriban.Runtime; +using UnityNuGet; +using UnityNuGet.Npm; using UnityNuGet.Server; namespace Microsoft.AspNetCore.Builder { public static class EndpointRouteBuilderExtensions { - public static void MapStatus(this IEndpointRouteBuilder builder) + public static void MapUnityNuGetEndpoints(this IEndpointRouteBuilder builder) + { + builder.MapHome(); + builder.MapGetAll(); + builder.MapGetPackage(); + builder.MapDownloadPackage(); + builder.MapStatus(); + } + + private static void MapHome(this IEndpointRouteBuilder builder) + { + builder.MapGet("/", () => Results.Redirect("/-/all")); + } + + private static void MapGetAll(this IEndpointRouteBuilder builder) + { + builder.MapGet("-/all", (RegistryCacheSingleton registryCacheSingleton, RegistryCacheReport registryCacheReport) => + { + if (!TryGetInstance(registryCacheSingleton, registryCacheReport, out RegistryCache? instance, out NpmError? error)) + { + return Results.Json(error, UnityNugetJsonSerializerContext.Default); + } + + NpmPackageListAllResponse? result = instance?.All(); + return Results.Json(result, UnityNugetJsonSerializerContext.Default); + }); + } + + private static void MapGetPackage(this IEndpointRouteBuilder builder) + { + builder.MapGet("{id}", (string id, RegistryCacheSingleton registryCacheSingleton, RegistryCacheReport registryCacheReport) => + { + if (!TryGetInstance(registryCacheSingleton, registryCacheReport, out RegistryCache? instance, out NpmError? error)) + { + return Results.Json(error, UnityNugetJsonSerializerContext.Default); + } + + NpmPackage? package = instance?.GetPackage(id); + if (package == null) + { + return Results.Json(NpmError.NotFound, UnityNugetJsonSerializerContext.Default); + } + + return Results.Json(package, UnityNugetJsonSerializerContext.Default); + }); + } + + private static void MapDownloadPackage(this IEndpointRouteBuilder builder) + { + builder.MapMethods("{id}/-/{file}", new[] { HttpMethod.Head.Method, HttpMethod.Get.Method }, handler: (string id, string file, HttpContext httpContext, RegistryCacheSingleton registryCacheSingleton, RegistryCacheReport registryCacheReport) => + { + if (!TryGetInstance(registryCacheSingleton, registryCacheReport, out RegistryCache? instance, out NpmError? error)) + { + return Results.Json(error, UnityNugetJsonSerializerContext.Default); + } + + NpmPackage? package = instance?.GetPackage(id); + if (package == null) + { + return Results.Json(NpmError.NotFound, UnityNugetJsonSerializerContext.Default); + } + + if (!file.StartsWith(id + "-") || !file.EndsWith(".tgz")) + { + return Results.Json(NpmError.NotFound, UnityNugetJsonSerializerContext.Default); + } + + string? filePath = instance?.GetPackageFilePath(file); + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + { + return Results.Json(NpmError.NotFound, UnityNugetJsonSerializerContext.Default); + } + + // This method can be called with HEAD request, so in that case we just calculate the content length + if (httpContext.Request.Method.Equals(HttpMethod.Head.Method)) + { + httpContext.Response.ContentType = "application/octet-stream"; + httpContext.Response.ContentLength = new FileInfo(filePath).Length; + return Results.Ok(); + } + else + { + return Results.File(filePath, "application/octet-stream", file); + } + }); + } + + private static void MapStatus(this IEndpointRouteBuilder builder) { const string text = @" @@ -83,6 +176,36 @@ public static void MapStatus(this IEndpointRouteBuilder builder) .Render(templateContext); await context.Response.WriteAsync(output); }); - } - } -} + } + + private static bool TryGetInstance(RegistryCacheSingleton registryCacheSingleton, RegistryCacheReport registryCacheReport, out RegistryCache? cacheInstance, out NpmError? npmError) + { + RegistryCache? instance = registryCacheSingleton.Instance; + + cacheInstance = instance; + npmError = instance == null ? GetNpmError(registryCacheReport) : null; + + return instance != null; + } + + private static NpmError GetNpmError(RegistryCacheReport registryCacheReport) + { + if (registryCacheReport.ErrorMessages.Any()) + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("Error initializing the server:"); + + foreach (string error in registryCacheReport.ErrorMessages) + { + stringBuilder.AppendLine(error); + } + + return new NpmError("not_initialized", stringBuilder.ToString()); + } + else + { + return new NpmError("not_initialized", $"The server is initializing ({registryCacheReport.Progress:F1}% completed). Please retry later..."); + } + } + } +} diff --git a/src/UnityNuGet.Server/Program.cs b/src/UnityNuGet.Server/Program.cs index 50583e69..b3d96272 100644 --- a/src/UnityNuGet.Server/Program.cs +++ b/src/UnityNuGet.Server/Program.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Builder; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -6,42 +8,53 @@ using UnityNuGet; using UnityNuGet.Server; -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -// Add the registry -builder.Services.AddSingleton(); -builder.Services.AddHostedService(serviceProvider => serviceProvider.GetRequiredService()); -// Add the registry cache initializer -builder.Services.AddHostedService(); -// Add the registry cache updater -builder.Services.AddHostedService(); -// Add the registry cache report -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -builder.Services.Configure(builder.Configuration.GetSection("Registry")); -builder.Services.AddSingleton, ValidateRegistryOptions>(); -builder.Services.AddOptionsWithValidateOnStart(); - -builder.Services.AddApplicationInsightsTelemetry(); - -// Also enable NewtonsoftJson serialization -builder.Services.AddControllers().AddNewtonsoftJson(); - -WebApplication app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseDeveloperExceptionPage(); - app.LogRequestHeaders(app.Services.GetRequiredService()); -} -else -{ - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} -app.UseRouting(); -app.MapControllers(); -app.MapStatus(); - -app.Run(); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// Add the registry +builder.Services.AddSingleton(); +builder.Services.AddHostedService(serviceProvider => serviceProvider.GetRequiredService()); +// Add the registry cache initializer +builder.Services.AddHostedService(); +// Add the registry cache updater +builder.Services.AddHostedService(); +// Add the registry cache report +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.Configure(builder.Configuration.GetSection("Registry")); +builder.Services.AddSingleton, ValidateRegistryOptions>(); +builder.Services.AddOptionsWithValidateOnStart(); + +builder.Services.AddApplicationInsightsTelemetry(); + +builder.Services.Configure(options => +{ + foreach (JsonConverter converter in UnityNugetJsonSerializerContext.Default.Options.Converters) + { + options.SerializerOptions.Converters.Add(converter); + } +}); + +builder.Services.AddHealthChecks(); + +WebApplication app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); + app.LogRequestHeaders(app.Services.GetRequiredService()); +} +else +{ + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} +app.UseRouting(); +app.MapHealthChecks("/health"); +app.MapUnityNuGetEndpoints(); + +app.Run(); + +public partial class Program +{ +} diff --git a/src/UnityNuGet.Server/Properties/launchSettings.json b/src/UnityNuGet.Server/Properties/launchSettings.json index 53004c36..6b3c4816 100644 --- a/src/UnityNuGet.Server/Properties/launchSettings.json +++ b/src/UnityNuGet.Server/Properties/launchSettings.json @@ -1,27 +1,27 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "UnityNuGetServer": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "-/all", - "applicationUrl": "http://localhost", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Docker": { - "commandName": "Docker", - "launchBrowser": false, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_URLS": "http://+:5000" - }, - "publishAllPorts": true, - "useSSL": true, - "httpPort": 5000, - "sslPort": 5001 - } - } -} \ No newline at end of file +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "UnityNuGetServer": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "http://localhost", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": false, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://+:5000" + }, + "publishAllPorts": true, + "useSSL": true, + "httpPort": 5000, + "sslPort": 5001 + } + } +} diff --git a/src/UnityNuGet.Server/RegistryCacheUpdater.cs b/src/UnityNuGet.Server/RegistryCacheUpdater.cs index 948fafb6..c80da2e3 100644 --- a/src/UnityNuGet.Server/RegistryCacheUpdater.cs +++ b/src/UnityNuGet.Server/RegistryCacheUpdater.cs @@ -12,8 +12,8 @@ namespace UnityNuGet.Server /// Update the RegistryCache at a regular interval /// internal sealed class RegistryCacheUpdater(Registry registry, RegistryCacheReport registryCacheReport, RegistryCacheSingleton currentRegistryCache, ILogger logger, IOptions registryOptionsAccessor) : BackgroundService - { - private readonly Registry _registry = registry; + { + private readonly Registry _registry = registry; private readonly RegistryCacheReport _registryCacheReport = registryCacheReport; private readonly RegistryCacheSingleton _currentRegistryCache = currentRegistryCache; private readonly ILogger _logger = logger; @@ -31,6 +31,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var newRegistryCache = new RegistryCache(_registry, _currentRegistryCache.UnityPackageFolder!, _currentRegistryCache.ServerUri!, _registryOptions.UnityScope!, _registryOptions.MinimumUnityVersion!, _registryOptions.PackageNameNuGetPostFix!, _registryOptions.TargetFrameworks!, _currentRegistryCache.NuGetRedirectLogger!) { + Filter = _registryOptions.Filter, // Update progress OnProgress = (current, total) => { @@ -60,15 +61,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Task.Delay((int)_registryOptions.UpdateInterval.TotalMilliseconds, stoppingToken); } - } - catch (TaskCanceledException) + } + catch (TaskCanceledException) { string message = "RegistryCache update canceled"; _logger.LogInformation("{Message}", message); _registryCacheReport.AddInformation($"{message}."); - _registryCacheReport.Complete(); + _registryCacheReport.Complete(); } catch (Exception ex) { diff --git a/src/UnityNuGet.Server/UnityNuGet.Server.csproj b/src/UnityNuGet.Server/UnityNuGet.Server.csproj index 746efbf3..48cd1b75 100644 --- a/src/UnityNuGet.Server/UnityNuGet.Server.csproj +++ b/src/UnityNuGet.Server/UnityNuGet.Server.csproj @@ -16,7 +16,6 @@ - diff --git a/src/UnityNuGet.Tests/NativeTests.cs b/src/UnityNuGet.Tests/NativeTests.cs index ca9748e2..7374e688 100644 --- a/src/UnityNuGet.Tests/NativeTests.cs +++ b/src/UnityNuGet.Tests/NativeTests.cs @@ -17,23 +17,23 @@ public class NativeTests { [Test] public async Task TestBuild() - { - var hostEnvironmentMock = new Mock(); - hostEnvironmentMock.Setup(h => h.EnvironmentName).Returns(Environments.Development); - - var loggerFactory = new LoggerFactory(); - loggerFactory.AddProvider(new FakeLoggerProvider()); + { + var hostEnvironmentMock = new Mock(); + hostEnvironmentMock.Setup(h => h.EnvironmentName).Returns(Environments.Development); + + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(new FakeLoggerProvider()); string unityPackages = Path.Combine(Path.GetDirectoryName(typeof(RegistryCacheTests).Assembly.Location)!, "unity_packages"); Directory.Delete(unityPackages, true); bool errorsTriggered = false; - - var registry = new Registry(hostEnvironmentMock.Object, loggerFactory, Options.Create(new RegistryOptions { RegistryFilePath = "registry.json" })); - - await registry.StartAsync(CancellationToken.None); - var registryCache = new RegistryCache( + var registry = new Registry(hostEnvironmentMock.Object, loggerFactory, Options.Create(new RegistryOptions { RegistryFilePath = "registry.json" })); + + await registry.StartAsync(CancellationToken.None); + + var registryCache = new RegistryCache( registry, unityPackages, new Uri("http://localhost/"), @@ -57,13 +57,13 @@ public async Task TestBuild() Assert.That(errorsTriggered, Is.False, "The registry failed to build, check the logs"); NpmPackageListAllResponse allResult = registryCache.All(); - string allResultJson = allResult.ToJson(); + string allResultJson = await allResult.ToJson(UnityNugetJsonSerializerContext.Default.NpmPackageListAllResponse); Assert.That(allResultJson, Does.Contain("org.nuget.rhino3dm")); NpmPackage? rhinoPackage = registryCache.GetPackage("org.nuget.rhino3dm"); Assert.That(rhinoPackage, Is.Not.Null); - string rhinopackageJson = rhinoPackage!.ToJson(); + string rhinopackageJson = await rhinoPackage!.ToJson(UnityNugetJsonSerializerContext.Default.NpmPackage); Assert.That(rhinopackageJson, Does.Contain("org.nuget.rhino3dm")); Assert.That(rhinopackageJson, Does.Contain("7.11.0")); } diff --git a/src/UnityNuGet.Tests/NpmPackageListAllResponseConverterTests.cs b/src/UnityNuGet.Tests/NpmPackageListAllResponseConverterTests.cs new file mode 100644 index 00000000..1ff7bf2f --- /dev/null +++ b/src/UnityNuGet.Tests/NpmPackageListAllResponseConverterTests.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Text.Json; +using NUnit.Framework; +using UnityNuGet.Npm; + +namespace UnityNuGet.Tests +{ + public class NpmPackageListAllResponseConverterTests + { + [Test] + public void Read_Write_Success() + { + string json = @"{""TestPackage"":{""name"":""TestPackage"",""description"":null,""maintainers"":[],""versions"":{},""time"":null,""keywords"":[],""author"":null}}"; + + NpmPackageListAllResponse response = JsonSerializer.Deserialize(json, UnityNugetJsonSerializerContext.Default.NpmPackageListAllResponse)!; + + Assert.That(response.Packages, Is.EquivalentTo(new Dictionary + { + { "TestPackage", new NpmPackageInfo { Name = "TestPackage" } } + }).Using(new NpmPackageInfoEqualityComparer())); + + string newJson = JsonSerializer.Serialize(response, UnityNugetJsonSerializerContext.Default.NpmPackageListAllResponse)!; + + Assert.That(newJson, Is.EqualTo(json)); + } + + private sealed class NpmPackageInfoEqualityComparer : IEqualityComparer + { + public bool Equals(NpmPackageInfo? p1, NpmPackageInfo? p2) + { + return p1 != null && p2 != null && p1.Name == p2.Name; + } + + public int GetHashCode(NpmPackageInfo obj) + { + return obj.Name!.GetHashCode(); + } + } + } +} diff --git a/src/UnityNuGet.Tests/RegistryCacheTests.cs b/src/UnityNuGet.Tests/RegistryCacheTests.cs index 0dc174e0..c9337a3c 100644 --- a/src/UnityNuGet.Tests/RegistryCacheTests.cs +++ b/src/UnityNuGet.Tests/RegistryCacheTests.cs @@ -58,14 +58,14 @@ public async Task TestBuild() NpmPackageListAllResponse allResult = registryCache.All(); Assert.That(allResult.Packages, Has.Count.GreaterThanOrEqualTo(3)); - string allResultJson = allResult.ToJson(); + string allResultJson = await allResult.ToJson(UnityNugetJsonSerializerContext.Default.NpmPackageListAllResponse); Assert.That(allResultJson, Does.Contain("org.nuget.scriban")); Assert.That(allResultJson, Does.Contain("org.nuget.system.runtime.compilerservices.unsafe")); NpmPackage? scribanPackage = registryCache.GetPackage("org.nuget.scriban"); Assert.That(scribanPackage, Is.Not.Null); - string scribanPackageJson = scribanPackage!.ToJson(); + string scribanPackageJson = await scribanPackage!.ToJson(UnityNugetJsonSerializerContext.Default.NpmPackage); Assert.That(scribanPackageJson, Does.Contain("org.nuget.scriban")); Assert.That(scribanPackageJson, Does.Contain("2.1.0")); } diff --git a/src/UnityNuGet.Tests/VersionRangeConverterTests.cs b/src/UnityNuGet.Tests/VersionRangeConverterTests.cs new file mode 100644 index 00000000..9ced619e --- /dev/null +++ b/src/UnityNuGet.Tests/VersionRangeConverterTests.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Text.Json; +using NuGet.Versioning; +using NUnit.Framework; + +namespace UnityNuGet.Tests +{ + public class VersionRangeConverterTests + { + [Test] + public void Read_Write_Success() + { + string json = @"{""ignore"":false,""listed"":false,""version"":""[1.2.3, )"",""defineConstraints"":[],""analyzer"":false}"; + + RegistryEntry registryEntry = JsonSerializer.Deserialize(json, UnityNugetJsonSerializerContext.Default.RegistryEntry)!; + + Assert.That(registryEntry, Is.EqualTo(new RegistryEntry + { + Version = new VersionRange(new NuGetVersion(1, 2, 3)) + }).Using(new RegistryEntryEqualityComparer())); + + string newJson = JsonSerializer.Serialize(registryEntry, UnityNugetJsonSerializerContext.Default.RegistryEntry)!; + + Assert.That(newJson, Is.EqualTo(json)); + } + + private sealed class RegistryEntryEqualityComparer : IEqualityComparer + { + public bool Equals(RegistryEntry? r1, RegistryEntry? r2) + { + return r1 != null && r2 != null && r1.Version?.MinVersion == r2.Version?.MinVersion; + } + + public int GetHashCode(RegistryEntry obj) + { + return obj.Version!.GetHashCode(); + } + } + } +} diff --git a/src/UnityNuGet.sln b/src/UnityNuGet.sln index a040a069..1960d10e 100644 --- a/src/UnityNuGet.sln +++ b/src/UnityNuGet.sln @@ -1,52 +1,58 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.4.33103.184 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnityNuGet.Server", "UnityNuGet.Server\UnityNuGet.Server.csproj", "{C84FC40C-94EE-4314-BB48-CD1A98849D82}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnityNuGet", "UnityNuGet\UnityNuGet.csproj", "{3E72FFD2-4CB6-4B44-9A49-29699722D93E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnityNuGet.Tests", "UnityNuGet.Tests\UnityNuGet.Tests.csproj", "{EBC6FFF4-FD7C-4E03-B907-F12908C1D900}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{D2A040A5-DB8C-43BE-A801-107503057C2B}" - ProjectSection(SolutionItems) = preProject - ..\changelog.md = ..\changelog.md - ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml - global.json = global.json - ..\license.txt = ..\license.txt - ..\readme.md = ..\readme.md - ..\registry.json = ..\registry.json - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EC78F52F-7E74-4570-BAA3-B0C55BD55C9B}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Release|Any CPU.Build.0 = Release|Any CPU - {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Release|Any CPU.Build.0 = Release|Any CPU - {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E40E3A93-4338-423A-B9D0-474BBA88F2A5} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnityNuGet.Server", "UnityNuGet.Server\UnityNuGet.Server.csproj", "{C84FC40C-94EE-4314-BB48-CD1A98849D82}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnityNuGet", "UnityNuGet\UnityNuGet.csproj", "{3E72FFD2-4CB6-4B44-9A49-29699722D93E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnityNuGet.Tests", "UnityNuGet.Tests\UnityNuGet.Tests.csproj", "{EBC6FFF4-FD7C-4E03-B907-F12908C1D900}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{D2A040A5-DB8C-43BE-A801-107503057C2B}" + ProjectSection(SolutionItems) = preProject + ..\changelog.md = ..\changelog.md + ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml + global.json = global.json + ..\license.txt = ..\license.txt + ..\readme.md = ..\readme.md + ..\registry.json = ..\registry.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EC78F52F-7E74-4570-BAA3-B0C55BD55C9B}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityNuGet.Server.Tests", "UnityNuGet.Server.Tests\UnityNuGet.Server.Tests.csproj", "{5733BC0F-D7E2-494C-AC40-67051FD921DE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Release|Any CPU.Build.0 = Release|Any CPU + {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Release|Any CPU.Build.0 = Release|Any CPU + {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Release|Any CPU.Build.0 = Release|Any CPU + {5733BC0F-D7E2-494C-AC40-67051FD921DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5733BC0F-D7E2-494C-AC40-67051FD921DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5733BC0F-D7E2-494C-AC40-67051FD921DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5733BC0F-D7E2-494C-AC40-67051FD921DE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E40E3A93-4338-423A-B9D0-474BBA88F2A5} + EndGlobalSection +EndGlobal diff --git a/src/UnityNuGet/JsonCommonExtensions.cs b/src/UnityNuGet/JsonCommonExtensions.cs index 11cbc89c..03c1c537 100644 --- a/src/UnityNuGet/JsonCommonExtensions.cs +++ b/src/UnityNuGet/JsonCommonExtensions.cs @@ -1,49 +1,26 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using NuGet.Versioning; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace UnityNuGet +{ + /// + /// Extension methods for serializing NPM JSON responses. + /// + public static class JsonCommonExtensions + { + public static async Task ToJson(this JsonObjectBase self, JsonTypeInfo jsonTypeInfo) + { + using var stream = new MemoryStream(); -namespace UnityNuGet -{ - /// - /// Extension methods for serializing NPM JSON responses. - /// - public static class JsonCommonExtensions - { - public static string ToJson(this JsonObjectBase self) => JsonConvert.SerializeObject(self, Settings); + await JsonSerializer.SerializeAsync(stream, self, jsonTypeInfo); - /// - /// Settings used for serializing JSON objects in this project. - /// - public static readonly JsonSerializerSettings Settings = new() - { - MetadataPropertyHandling = MetadataPropertyHandling.Ignore, - DateParseHandling = DateParseHandling.None, - Converters = - { - new IsoDateTimeConverter { - DateTimeStyles = DateTimeStyles.AssumeUniversal - }, - new VersionConverter(), - }, - }; + stream.Position = 0; - /// - /// Converter for NuGet - /// - private class VersionConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, VersionRange? value, JsonSerializer serializer) - { - writer.WriteValue(value?.ToString()); - } + using var reader = new StreamReader(stream); - public override VersionRange? ReadJson(JsonReader reader, Type objectType, VersionRange? existingValue, bool hasExistingValue, JsonSerializer serializer) - { - string? s = (string?)reader.Value; - return VersionRange.Parse(s!); - } - } - } -} + return await reader.ReadToEndAsync(); + } + } +} diff --git a/src/UnityNuGet/Npm/NpmDistribution.cs b/src/UnityNuGet/Npm/NpmDistribution.cs index b32b9c7f..936e93f3 100644 --- a/src/UnityNuGet/Npm/NpmDistribution.cs +++ b/src/UnityNuGet/Npm/NpmDistribution.cs @@ -1,17 +1,17 @@ -using System; -using Newtonsoft.Json; - -namespace UnityNuGet.Npm -{ - /// - /// Describes how a NPM package is distributed, used by - /// - public class NpmDistribution : NpmObject - { - [JsonProperty("tarball")] - public Uri? Tarball { get; set; } - - [JsonProperty("shasum")] - public string? Shasum { get; set; } - } -} +using System; +using System.Text.Json.Serialization; + +namespace UnityNuGet.Npm +{ + /// + /// Describes how a NPM package is distributed, used by + /// + public class NpmDistribution : NpmObject + { + [JsonPropertyName("tarball")] + public Uri? Tarball { get; set; } + + [JsonPropertyName("shasum")] + public string? Shasum { get; set; } + } +} diff --git a/src/UnityNuGet/Npm/NpmError.cs b/src/UnityNuGet/Npm/NpmError.cs index 17a8a992..665ec4ae 100644 --- a/src/UnityNuGet/Npm/NpmError.cs +++ b/src/UnityNuGet/Npm/NpmError.cs @@ -1,18 +1,19 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace UnityNuGet.Npm { /// /// A simple object to return npm errors. Used mainly for returning - /// + /// + [method: JsonConstructor] public class NpmError(string error, string reason) : NpmObject { public static readonly NpmError NotFound = new("not_found", "document not found"); - [JsonProperty("error")] + [JsonPropertyName("error")] public string Error { get; } = error; - [JsonProperty("reason")] + [JsonPropertyName("reason")] public string Reason { get; } = reason; } } diff --git a/src/UnityNuGet/Npm/NpmObject.cs b/src/UnityNuGet/Npm/NpmObject.cs index ed529867..817f0219 100644 --- a/src/UnityNuGet/Npm/NpmObject.cs +++ b/src/UnityNuGet/Npm/NpmObject.cs @@ -1,10 +1,9 @@ -namespace UnityNuGet.Npm -{ - /// - /// Base object for NpmObjects. - /// - public abstract class NpmObject : JsonObjectBase - { - - } -} +namespace UnityNuGet.Npm +{ + /// + /// Base object for NpmObjects. + /// + public abstract class NpmObject : JsonObjectBase + { + } +} diff --git a/src/UnityNuGet/Npm/NpmPackage.cs b/src/UnityNuGet/Npm/NpmPackage.cs index 5275f683..c975d440 100644 --- a/src/UnityNuGet/Npm/NpmPackage.cs +++ b/src/UnityNuGet/Npm/NpmPackage.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace UnityNuGet.Npm { @@ -9,43 +9,34 @@ namespace UnityNuGet.Npm /// public class NpmPackage : NpmObject { - public NpmPackage() - { - Revision = "1-0"; - DistTags = []; - Versions = []; - Time = []; - Users = []; - } - - [JsonProperty("_id")] + [JsonPropertyName("_id")] public string? Id { get; set; } - [JsonProperty("_rev")] - public string Revision { get; set; } + [JsonPropertyName("_rev")] + public string Revision { get; set; } = "1-0"; - [JsonProperty("name")] + [JsonPropertyName("name")] public string? Name { get; set; } - [JsonProperty("license")] + [JsonPropertyName("license")] public string? License { get; set; } - [JsonProperty("description")] + [JsonPropertyName("description")] public string? Description { get; set; } - [JsonProperty("dist-tags")] - public Dictionary DistTags { get; } + [JsonPropertyName("dist-tags")] + public Dictionary DistTags { get; } = []; - [JsonProperty("versions")] - public Dictionary Versions { get; } + [JsonPropertyName("versions")] + public Dictionary Versions { get; } = []; - [JsonProperty("time")] - public Dictionary Time { get; } + [JsonPropertyName("time")] + public Dictionary Time { get; } = []; - [JsonProperty("repository", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("repository")] public NpmSourceRepository? Repository { get; set; } - [JsonProperty("users")] - public Dictionary Users { get; } + [JsonPropertyName("users")] + public Dictionary Users { get; } = []; } } diff --git a/src/UnityNuGet/Npm/NpmPackageCacheEntry.cs b/src/UnityNuGet/Npm/NpmPackageCacheEntry.cs index 9ce1fce4..efb6a61c 100644 --- a/src/UnityNuGet/Npm/NpmPackageCacheEntry.cs +++ b/src/UnityNuGet/Npm/NpmPackageCacheEntry.cs @@ -1,20 +1,20 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. -// Licensed under the BSD-Clause 2 license. -// See license.txt file in the project root for full license information. - -using Newtonsoft.Json; - -namespace UnityNuGet.Npm -{ - public class NpmPackageCacheEntry : NpmObject - { - [JsonProperty("package")] - public NpmPackage? Package { get; set; } - - [JsonProperty("info")] - public NpmPackageInfo? Info { get; set; } - - [JsonIgnore] - public string? Json { get; set; } - } -} +// Copyright (c) Alexandre Mutel. All rights reserved. +// Licensed under the BSD-Clause 2 license. +// See license.txt file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace UnityNuGet.Npm +{ + public class NpmPackageCacheEntry : NpmObject + { + [JsonPropertyName("package")] + public NpmPackage? Package { get; set; } + + [JsonPropertyName("info")] + public NpmPackageInfo? Info { get; set; } + + [JsonIgnore] + public string? Json { get; set; } + } +} diff --git a/src/UnityNuGet/Npm/NpmPackageInfo.cs b/src/UnityNuGet/Npm/NpmPackageInfo.cs index b2401a70..ac63f85e 100644 --- a/src/UnityNuGet/Npm/NpmPackageInfo.cs +++ b/src/UnityNuGet/Npm/NpmPackageInfo.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace UnityNuGet.Npm { @@ -9,32 +9,25 @@ namespace UnityNuGet.Npm /// public class NpmPackageInfo : NpmObject { - public NpmPackageInfo() - { - Maintainers = []; - Versions = []; - Keywords = []; - } - - [JsonProperty("name")] + [JsonPropertyName("name")] public string? Name { get; set; } - [JsonProperty("description")] + [JsonPropertyName("description")] public string? Description { get; set; } - [JsonProperty("maintainers")] - public List Maintainers { get; } + [JsonPropertyName("maintainers")] + public List Maintainers { get; } = []; - [JsonProperty("versions")] - public Dictionary Versions { get; } + [JsonPropertyName("versions")] + public Dictionary Versions { get; } = []; - [JsonProperty("time")] + [JsonPropertyName("time")] public DateTimeOffset? Time { get; set; } - [JsonProperty("keywords")] - public List Keywords { get; } + [JsonPropertyName("keywords")] + public List Keywords { get; } = []; - [JsonProperty("author")] + [JsonPropertyName("author")] public string? Author { get; set; } } } diff --git a/src/UnityNuGet/Npm/NpmPackageListAllResponse.cs b/src/UnityNuGet/Npm/NpmPackageListAllResponse.cs index f45a1b76..5b06ff17 100644 --- a/src/UnityNuGet/Npm/NpmPackageListAllResponse.cs +++ b/src/UnityNuGet/Npm/NpmPackageListAllResponse.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json.Serialization; namespace UnityNuGet.Npm { @@ -12,33 +11,11 @@ namespace UnityNuGet.Npm /// public class NpmPackageListAllResponse : NpmObject { - public NpmPackageListAllResponse() - { - Unused = 99999; - Packages = []; - } - - [JsonProperty("_updated")] - public int Unused { get; set; } + [JsonPropertyName("_updated")] + public int Updated { get; set; } = 99999; [JsonIgnore] - public Dictionary Packages { get; } - - // Everything else gets stored here - [JsonExtensionData] - private IDictionary AdditionalData - { - get - { - var marshalPackages = new Dictionary(); - foreach (KeyValuePair packagePair in Packages) - { - marshalPackages.Add(packagePair.Key, JObject.FromObject(packagePair.Value)); - } - - return marshalPackages; - } - } + public Dictionary Packages { get; } = []; public void Reset() { diff --git a/src/UnityNuGet/Npm/NpmPackageVersion.cs b/src/UnityNuGet/Npm/NpmPackageVersion.cs index 932f7f92..bb868964 100644 --- a/src/UnityNuGet/Npm/NpmPackageVersion.cs +++ b/src/UnityNuGet/Npm/NpmPackageVersion.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace UnityNuGet.Npm { @@ -8,44 +8,37 @@ namespace UnityNuGet.Npm /// public class NpmPackageVersion : NpmObject { - public NpmPackageVersion() - { - Dependencies = []; - Distribution = new(); - Scripts = []; - } - - [JsonProperty("name")] + [JsonPropertyName("name")] public string? Name { get; set; } - [JsonProperty("version")] + [JsonPropertyName("version")] public string? Version { get; set; } - [JsonProperty("dist")] - public NpmDistribution Distribution { get; } + [JsonPropertyName("dist")] + public NpmDistribution Distribution { get; } = new(); - [JsonProperty("dependencies")] - public Dictionary Dependencies { get; } + [JsonPropertyName("dependencies")] + public Dictionary Dependencies { get; } = []; - [JsonProperty("_id")] + [JsonPropertyName("_id")] public string? Id { get; set; } - [JsonProperty("unity", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("unity")] public string? Unity { get; set; } - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("description")] public string? Description { get; set; } - [JsonProperty("displayName", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("displayName")] public string? DisplayName { get; set; } - [JsonProperty("scripts", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Scripts { get; } + [JsonPropertyName("scripts")] + public Dictionary Scripts { get; } = []; - [JsonProperty("repository", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("repository")] public NpmSourceRepository? Repository { get; set; } - [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("author")] public string? Author { get; set; } } } diff --git a/src/UnityNuGet/Npm/NpmSourceRepository.cs b/src/UnityNuGet/Npm/NpmSourceRepository.cs index b0f5e5ae..65cf8460 100644 --- a/src/UnityNuGet/Npm/NpmSourceRepository.cs +++ b/src/UnityNuGet/Npm/NpmSourceRepository.cs @@ -1,21 +1,21 @@ -using Newtonsoft.Json; - -namespace UnityNuGet.Npm -{ - /// - /// Describes a source repository used both by and - /// - public partial class NpmSourceRepository : NpmObject - { - [JsonProperty("type")] - public string? Type { get; set; } - - [JsonProperty("url")] - public string? Url { get; set; } - - [JsonProperty("revision")] - public string? Revision { get; set; } - - public NpmSourceRepository Clone() => (NpmSourceRepository)MemberwiseClone(); - } -} +using System.Text.Json.Serialization; + +namespace UnityNuGet.Npm +{ + /// + /// Describes a source repository used both by and + /// + public partial class NpmSourceRepository : NpmObject + { + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("revision")] + public string? Revision { get; set; } + + public NpmSourceRepository Clone() => (NpmSourceRepository)MemberwiseClone(); + } +} diff --git a/src/UnityNuGet/NpmPackageListAllResponseConverter.cs b/src/UnityNuGet/NpmPackageListAllResponseConverter.cs new file mode 100644 index 00000000..b53b1904 --- /dev/null +++ b/src/UnityNuGet/NpmPackageListAllResponseConverter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using UnityNuGet.Npm; + +namespace UnityNuGet +{ + /// + /// Converter for NuGet + /// + internal sealed class NpmPackageListAllResponseConverter : JsonConverter + { + public override NpmPackageListAllResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + NpmPackageListAllResponse result = new(); + + string? currentPropertyName = null; + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + result.Packages.Add(currentPropertyName!, JsonSerializer.Deserialize(ref reader, UnityNugetJsonSerializerContext.Default.NpmPackageInfo)!); + break; + case JsonTokenType.PropertyName: + currentPropertyName = reader.GetString(); + break; + } + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, NpmPackageListAllResponse value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (KeyValuePair kvp in value.Packages) + { + writer.WritePropertyName(kvp.Key); + + JsonSerializer.Serialize(writer, kvp.Value, UnityNugetJsonSerializerContext.Default.NpmPackageInfo); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/UnityNuGet/Registry.cs b/src/UnityNuGet/Registry.cs index 17db7380..52faf46d 100644 --- a/src/UnityNuGet/Registry.cs +++ b/src/UnityNuGet/Registry.cs @@ -2,12 +2,12 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; namespace UnityNuGet { @@ -60,11 +60,11 @@ public async Task StartAsync(CancellationToken cancellationToken) logger.LogInformation("Using Unity registry file `{UnityRegistryFile}`", registryFilePath); - string json = await File.ReadAllTextAsync(registryFilePath, cancellationToken); + using Stream stream = File.OpenRead(registryFilePath); - IDictionary data = JsonConvert.DeserializeObject>(json, JsonCommonExtensions.Settings)!; + IDictionary? data = await JsonSerializer.DeserializeAsync(stream, UnityNugetJsonSerializerContext.Default.IDictionaryStringRegistryEntry, cancellationToken); - _data = new Dictionary(data, StringComparer.OrdinalIgnoreCase); + _data = new Dictionary(data!, StringComparer.OrdinalIgnoreCase); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/src/UnityNuGet/RegistryCache.cs b/src/UnityNuGet/RegistryCache.cs index b3e657f8..01bcdc17 100644 --- a/src/UnityNuGet/RegistryCache.cs +++ b/src/UnityNuGet/RegistryCache.cs @@ -6,12 +6,12 @@ using System.Net.Http; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; -using Newtonsoft.Json; using NuGet.Common; using NuGet.Configuration; using NuGet.Frameworks; @@ -210,8 +210,8 @@ private async Task BuildInternal() _npmPackageRegistry.Reset(); } - Regex? regexFilter = Filter != null ? new Regex(Filter, RegexOptions.IgnoreCase) : null; - if (Filter != null) + Regex? regexFilter = !string.IsNullOrEmpty(Filter) ? new Regex(Filter, RegexOptions.IgnoreCase) : null; + if (regexFilter != null) { LogInformation($"Filtering with regex: {Filter}"); } @@ -344,7 +344,8 @@ private async Task BuildInternal() Name = npmPackageId, Description = packageMeta.Description, Author = npmPackageInfo.Author, - DisplayName = packageMeta.Title + _packageNameNuGetPostFix + DisplayName = packageMeta.Title + _packageNameNuGetPostFix, + Repository = npmPackage.Repository }; npmVersion.Distribution.Tarball = new Uri(_rootHttpUri, $"{npmPackage.Id}/-/{GetUnityPackageFileName(packageIdentity, npmVersion)}"); npmVersion.Unity = _minimumUnityVersion; @@ -682,7 +683,7 @@ RegistryEntry packageEntry // Write analyzer asmdef // Check Analyzer Scope section: https://docs.unity3d.com/Manual/roslyn-analyzers.html UnityAsmdef analyzerAsmdef = CreateAnalyzerAmsdef(identity); - string analyzerAsmdefAsJson = analyzerAsmdef.ToJson(); + string analyzerAsmdefAsJson = await analyzerAsmdef.ToJson(UnityNugetJsonSerializerContext.Default.UnityAsmdef); string analyzerAsmdefFileName = $"{identity.Id}.asmdef"; await WriteTextFileToTar(tarArchive, analyzerAsmdefFileName, analyzerAsmdefAsJson, modTime); await WriteTextFileToTar(tarArchive, $"{analyzerAsmdefFileName}.meta", UnityMeta.GetMetaForExtension(GetStableGuid(identity, analyzerAsmdefFileName), ".asmdef")!, modTime); @@ -878,7 +879,7 @@ RegistryEntry packageEntry // Write the package,json UnityPackage unityPackage = CreateUnityPackage(npmPackageInfo, npmPackageVersion); - string unityPackageAsJson = unityPackage.ToJson(); + string unityPackageAsJson = await unityPackage.ToJson(UnityNugetJsonSerializerContext.Default.UnityPackage); const string packageJsonFileName = "package.json"; await WriteTextFileToTar(tarArchive, packageJsonFileName, unityPackageAsJson, modTime); await WriteTextFileToTar(tarArchive, $"{packageJsonFileName}.meta", UnityMeta.GetMetaForExtension(GetStableGuid(identity, packageJsonFileName), ".json")!, modTime); @@ -1039,7 +1040,7 @@ private bool TryReadPackageCacheEntry(string packageName, [NotNullWhen(true)] ou try { string cacheEntryAsJson = File.ReadAllText(path); - cacheEntry = JsonConvert.DeserializeObject(cacheEntryAsJson, JsonCommonExtensions.Settings); + cacheEntry = JsonSerializer.Deserialize(cacheEntryAsJson, UnityNugetJsonSerializerContext.Default.NpmPackageCacheEntry); if (cacheEntry != null) { cacheEntry.Json = cacheEntryAsJson; @@ -1057,11 +1058,11 @@ private bool TryReadPackageCacheEntry(string packageName, [NotNullWhen(true)] ou private async Task WritePackageCacheEntry(string packageName, NpmPackageCacheEntry cacheEntry) { string path = GetUnityPackageDescPath(packageName); - string newJson = cacheEntry.ToJson(); + string newJson = await cacheEntry.ToJson(UnityNugetJsonSerializerContext.Default.NpmPackageCacheEntry); // Only update if entry is different if (!string.Equals(newJson, cacheEntry.Json, StringComparison.InvariantCulture)) { - await File.WriteAllTextAsync(path, cacheEntry.ToJson()); + await File.WriteAllTextAsync(path, await cacheEntry.ToJson(UnityNugetJsonSerializerContext.Default.NpmPackageCacheEntry)); } } diff --git a/src/UnityNuGet/RegistryEntry.cs b/src/UnityNuGet/RegistryEntry.cs index 8c00a404..6449fc89 100644 --- a/src/UnityNuGet/RegistryEntry.cs +++ b/src/UnityNuGet/RegistryEntry.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NuGet.Versioning; namespace UnityNuGet @@ -9,19 +9,19 @@ namespace UnityNuGet /// public class RegistryEntry { - [JsonProperty("ignore")] + [JsonPropertyName("ignore")] public bool Ignored { get; set; } - [JsonProperty("listed")] + [JsonPropertyName("listed")] public bool Listed { get; set; } - [JsonProperty("version")] + [JsonPropertyName("version")] public VersionRange? Version { get; set; } - [JsonProperty("defineConstraints")] + [JsonPropertyName("defineConstraints")] public List DefineConstraints { get; set; } = []; - [JsonProperty("analyzer")] + [JsonPropertyName("analyzer")] public bool Analyzer { get; set; } } } diff --git a/src/UnityNuGet/RegistryOptions.cs b/src/UnityNuGet/RegistryOptions.cs index 2f94d9f9..499b0744 100644 --- a/src/UnityNuGet/RegistryOptions.cs +++ b/src/UnityNuGet/RegistryOptions.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using NuGet.Frameworks; namespace UnityNuGet @@ -11,6 +11,8 @@ public class RegistryOptions [Required] public Uri? RootHttpUrl { get; set; } + public string? Filter { get; set; } + [Required] [RegularExpression(@"[a-z]+\.[a-z]+")] public string? UnityScope { get; set; } diff --git a/src/UnityNuGet/UnityAsmdef.cs b/src/UnityNuGet/UnityAsmdef.cs index 859fe98a..844dc490 100644 --- a/src/UnityNuGet/UnityAsmdef.cs +++ b/src/UnityNuGet/UnityAsmdef.cs @@ -1,14 +1,14 @@ -using Newtonsoft.Json; - -namespace UnityNuGet -{ - public class UnityAsmdef : JsonObjectBase - { - [JsonProperty("name")] - public string? Name { get; set; } - - // The values come from: https://docs.unity3d.com/ScriptReference/BuildTarget.html - [JsonProperty("includePlatforms")] - public string[]? IncludePlatforms { get; set; } - } -} +using System.Text.Json.Serialization; + +namespace UnityNuGet +{ + public class UnityAsmdef : JsonObjectBase + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + // The values come from: https://docs.unity3d.com/ScriptReference/BuildTarget.html + [JsonPropertyName("includePlatforms")] + public string[]? IncludePlatforms { get; set; } + } +} diff --git a/src/UnityNuGet/UnityNugetJsonSerializerContext.cs b/src/UnityNuGet/UnityNugetJsonSerializerContext.cs new file mode 100644 index 00000000..e630004a --- /dev/null +++ b/src/UnityNuGet/UnityNugetJsonSerializerContext.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using UnityNuGet.Npm; + +namespace UnityNuGet +{ + [JsonSourceGenerationOptions(Converters = [typeof(NpmPackageListAllResponseConverter), typeof(VersionRangeConverter)])] + [JsonSerializable(typeof(NpmError))] + [JsonSerializable(typeof(NpmPackage))] + [JsonSerializable(typeof(NpmPackageCacheEntry))] + [JsonSerializable(typeof(NpmPackageListAllResponse))] + [JsonSerializable(typeof(Registry))] + [JsonSerializable(typeof(UnityAsmdef))] + [JsonSerializable(typeof(UnityPackage))] + [JsonSerializable(typeof(IDictionary))] + public sealed partial class UnityNugetJsonSerializerContext : JsonSerializerContext + { + } +} diff --git a/src/UnityNuGet/UnityPackage.cs b/src/UnityNuGet/UnityPackage.cs index b07b41d8..d83de8cb 100644 --- a/src/UnityNuGet/UnityPackage.cs +++ b/src/UnityNuGet/UnityPackage.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace UnityNuGet { @@ -8,34 +8,28 @@ namespace UnityNuGet /// public class UnityPackage : JsonObjectBase { - public UnityPackage() - { - Keywords = []; - Dependencies = []; - } - - [JsonProperty("name")] + [JsonPropertyName("name")] public string? Name { get; set; } - [JsonProperty("displayName")] + [JsonPropertyName("displayName")] public string? DisplayName { get; set; } - [JsonProperty("version")] + [JsonPropertyName("version")] public string? Version { get; set; } - [JsonProperty("unity")] + [JsonPropertyName("unity")] public string? Unity { get; set; } - [JsonProperty("description")] + [JsonPropertyName("description")] public string? Description { get; set; } - [JsonProperty("keywords")] - public List Keywords { get; } + [JsonPropertyName("keywords")] + public List Keywords { get; } = []; - [JsonProperty("category")] + [JsonPropertyName("category")] public string? Category { get; set; } - [JsonProperty("dependencies")] - public Dictionary Dependencies { get; } + [JsonPropertyName("dependencies")] + public Dictionary Dependencies { get; } = []; } } diff --git a/src/UnityNuGet/VersionRangeConverter.cs b/src/UnityNuGet/VersionRangeConverter.cs new file mode 100644 index 00000000..4d702513 --- /dev/null +++ b/src/UnityNuGet/VersionRangeConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using NuGet.Versioning; + +namespace UnityNuGet +{ + /// + /// Converter for NuGet + /// + internal sealed class VersionRangeConverter : JsonConverter + { + public override VersionRange? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? s = reader.GetString(); + return VersionRange.Parse(s!); + } + + public override void Write(Utf8JsonWriter writer, VersionRange value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString()); + } + } +}