From ec7516d6a67ffb8a19714c3bed09737c24f27f01 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Wed, 20 Sep 2023 08:09:57 -0500 Subject: [PATCH] refactor: Replace Newtonsoft.Json with System.Text.Json --- src/Directory.Packages.props | 8 +- src/Recyclarr.Cli/Cache/CachePersister.cs | 4 +- src/Recyclarr.Cli/Cache/ServiceCache.cs | 14 ++- .../CustomFormat/Api/CustomFormatService.cs | 2 +- .../QualityProfile/Api/QualityProfileDto.cs | 2 +- .../QualityProfileStatCalculator.cs | 8 +- .../Api/Objects/SonarrReleaseProfile.cs | 6 +- .../ErrorHandling/ErrorResponseParser.cs | 25 +++--- .../FlurlHttpExceptionHandler.cs | 4 +- src/Recyclarr.Cli/Recyclarr.Cli.csproj | 1 + src/Recyclarr.Common/Recyclarr.Common.csproj | 1 - src/Recyclarr.Json/CollectionJsonConverter.cs | 53 ++++++++++++ .../GlobalJsonSerializerSettings.cs | 43 ++++++++++ src/Recyclarr.Json/GlobalUsings.cs | 2 + .../JsonAutofacModule.cs | 7 +- src/Recyclarr.Json/JsonExtensions.cs | 21 +++++ .../JsonSerializationModifiers.cs | 41 +++++++++ .../JsonUtils.cs | 2 +- .../Loading}/BulkJsonLoader.cs | 13 ++- .../Loading}/GuideJsonLoader.cs | 6 +- .../Loading}/IBulkJsonLoader.cs | 2 +- .../Loading}/ServiceJsonLoader.cs | 6 +- .../ReadOnlyCollectionJsonConverter.cs | 51 +++++++++++ src/Recyclarr.Json/Recyclarr.Json.csproj | 14 +++ .../ConfigTemplateGuideService.cs | 16 ++-- .../CustomFormat/CustomFormatLoader.cs | 2 +- .../QualitySize/QualitySizeGuideParser.cs | 17 +--- .../ReleaseProfile/ReleaseProfileData.cs | 6 +- .../ReleaseProfileGuideParser.cs | 25 +++--- .../ReleaseProfile/TermDataConverter.cs | 35 +++----- .../Http/FlurlClientFactory.cs | 4 +- src/Recyclarr.TrashLib/Http/FlurlLogging.cs | 4 +- .../Json/GlobalJsonSerializerSettings.cs | 38 -------- .../Json/ServiceContractResolver.cs | 51 ----------- .../Models/CustomFormatData.cs | 10 ++- .../Models/FieldValueConverter.cs | 22 +++++ .../Models/FieldsArrayJsonConverter.cs | 35 ++++---- .../Recyclarr.TrashLib.csproj | 1 + .../Repo/TrashRepoMetadataBuilder.cs | 10 +-- .../TrashLibAutofacModule.cs | 2 +- src/Recyclarr.sln | 20 +++++ .../Cache/ServiceCacheTest.cs | 15 ++-- .../Models/FieldsArrayJsonConverterTest.cs | 44 ++++------ .../JsonIntegrationFixture.cs | 14 +++ .../Recyclarr.Json.TestLibrary.csproj | 14 +++ .../BulkJsonLoaderIntegrationTest.cs | 27 +++--- .../JsonUtilsTest.cs | 2 +- .../Recyclarr.Json.Tests.csproj | 7 ++ .../FluentAssertions/JsonEquivalencyStep.cs | 27 ------ .../IntegrationTestFixture.cs | 86 +++++++++++++++++++ src/tests/Recyclarr.TestLibrary/MockData.cs | 6 +- .../Parsing/ConfigurationLoaderTest.cs | 4 +- .../CustomFormatLoaderIntegrationTest.cs | 8 +- .../ReleaseProfileGuideServiceTest.cs | 21 ++--- .../TrashLibIntegrationFixture.cs | 83 +++--------------- 55 files changed, 591 insertions(+), 401 deletions(-) create mode 100644 src/Recyclarr.Json/CollectionJsonConverter.cs create mode 100644 src/Recyclarr.Json/GlobalJsonSerializerSettings.cs create mode 100644 src/Recyclarr.Json/GlobalUsings.cs rename src/{Recyclarr.TrashLib/Json => Recyclarr.Json}/JsonAutofacModule.cs (80%) create mode 100644 src/Recyclarr.Json/JsonExtensions.cs create mode 100644 src/Recyclarr.Json/JsonSerializationModifiers.cs rename src/{Recyclarr.Common => Recyclarr.Json}/JsonUtils.cs (94%) rename src/{Recyclarr.TrashLib/Json => Recyclarr.Json/Loading}/BulkJsonLoader.cs (79%) rename src/{Recyclarr.TrashLib/Json => Recyclarr.Json/Loading}/GuideJsonLoader.cs (75%) rename src/{Recyclarr.TrashLib/Json => Recyclarr.Json/Loading}/IBulkJsonLoader.cs (87%) rename src/{Recyclarr.TrashLib/Json => Recyclarr.Json/Loading}/ServiceJsonLoader.cs (75%) create mode 100644 src/Recyclarr.Json/ReadOnlyCollectionJsonConverter.cs create mode 100644 src/Recyclarr.Json/Recyclarr.Json.csproj delete mode 100644 src/Recyclarr.TrashLib/Json/GlobalJsonSerializerSettings.cs delete mode 100644 src/Recyclarr.TrashLib/Json/ServiceContractResolver.cs create mode 100644 src/Recyclarr.TrashLib/Models/FieldValueConverter.cs create mode 100644 src/tests/Recyclarr.Json.TestLibrary/JsonIntegrationFixture.cs create mode 100644 src/tests/Recyclarr.Json.TestLibrary/Recyclarr.Json.TestLibrary.csproj rename src/tests/{Recyclarr.TrashLib.Tests/Json => Recyclarr.Json.Tests}/BulkJsonLoaderIntegrationTest.cs (60%) rename src/tests/{Recyclarr.Common.Tests => Recyclarr.Json.Tests}/JsonUtilsTest.cs (98%) create mode 100644 src/tests/Recyclarr.Json.Tests/Recyclarr.Json.Tests.csproj delete mode 100644 src/tests/Recyclarr.TestLibrary/FluentAssertions/JsonEquivalencyStep.cs create mode 100644 src/tests/Recyclarr.TestLibrary/IntegrationTestFixture.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9716e234..7b0a4e25 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -12,11 +12,11 @@ - + + - @@ -30,6 +30,8 @@ + + @@ -63,4 +65,4 @@ - + \ No newline at end of file diff --git a/src/Recyclarr.Cli/Cache/CachePersister.cs b/src/Recyclarr.Cli/Cache/CachePersister.cs index 69b4f336..7e43a48a 100644 --- a/src/Recyclarr.Cli/Cache/CachePersister.cs +++ b/src/Recyclarr.Cli/Cache/CachePersister.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json; using Recyclarr.TrashLib.Config; namespace Recyclarr.Cli.Cache; @@ -37,7 +37,7 @@ public class CachePersister : ICachePersister public void Save(IServiceConfiguration config, CustomFormatCache cache) { - _log.Debug("Saving Cache with {Mappings}", JsonConvert.SerializeObject(cache.TrashIdMappings)); + _log.Debug("Saving Cache with {Mappings}", JsonSerializer.Serialize(cache.TrashIdMappings)); _cache.Save(cache, config); } diff --git a/src/Recyclarr.Cli/Cache/ServiceCache.cs b/src/Recyclarr.Cli/Cache/ServiceCache.cs index 995ac063..d7ff7f6e 100644 --- a/src/Recyclarr.Cli/Cache/ServiceCache.cs +++ b/src/Recyclarr.Cli/Cache/ServiceCache.cs @@ -1,18 +1,18 @@ using System.IO.Abstractions; using System.Reflection; +using System.Text.Json; using System.Text.RegularExpressions; -using Newtonsoft.Json; using Recyclarr.Common.Extensions; +using Recyclarr.Json; using Recyclarr.TrashLib.Config; using Recyclarr.TrashLib.Interfaces; -using Recyclarr.TrashLib.Json; namespace Recyclarr.Cli.Cache; public partial class ServiceCache : IServiceCache { private readonly ICacheStoragePath _storagePath; - private readonly JsonSerializerSettings _jsonSettings; + private readonly JsonSerializerOptions _jsonSettings; private readonly ILogger _log; public ServiceCache(ICacheStoragePath storagePath, ILogger log) @@ -37,7 +37,7 @@ public partial class ServiceCache : IServiceCache try { - return JsonConvert.DeserializeObject(json, _jsonSettings); + return JsonSerializer.Deserialize(json, _jsonSettings); } catch (JsonException e) { @@ -53,10 +53,8 @@ public partial class ServiceCache : IServiceCache _log.Debug("Saving cache to path: {Path}", path.FullName); path.CreateParentDirectory(); - var serializer = JsonSerializer.Create(_jsonSettings); - - using var stream = new JsonTextWriter(path.CreateText()); - serializer.Serialize(stream, obj); + using var stream = path.Create(); + JsonSerializer.Serialize(stream, obj, _jsonSettings); } private static string GetCacheObjectNameAttribute() diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/Api/CustomFormatService.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/Api/CustomFormatService.cs index 9c8180a2..01b4c8cc 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/Api/CustomFormatService.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/Api/CustomFormatService.cs @@ -39,6 +39,6 @@ public class CustomFormatService : ICustomFormatService CancellationToken cancellationToken = default) { await _service.Request(config, "customformat", customFormatId) - .DeleteAsync(cancellationToken); + .DeleteAsync(cancellationToken: cancellationToken); } } diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/Api/QualityProfileDto.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/Api/QualityProfileDto.cs index db9f39c8..78067483 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/Api/QualityProfileDto.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/Api/QualityProfileDto.cs @@ -1,5 +1,5 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; namespace Recyclarr.Cli.Pipelines.QualityProfile.Api; diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileStatCalculator.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileStatCalculator.cs index 27ed1e63..35efe8c5 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileStatCalculator.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileStatCalculator.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.JsonDiffPatch; using Recyclarr.Cli.Pipelines.QualityProfile.Api; namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases; @@ -55,8 +56,9 @@ public class QualityProfileStatCalculator private static void QualityUpdates(ProfileWithStats stats, QualityProfileDto oldDto, QualityProfileDto newDto) { - stats.QualitiesChanged = !JToken.DeepEquals( - JToken.FromObject(oldDto.Items), JToken.FromObject(newDto.Items)); + using var oldJson = JsonSerializer.SerializeToDocument(oldDto.Items); + using var newJson = JsonSerializer.SerializeToDocument(newDto.Items); + stats.QualitiesChanged = !oldJson.DeepEquals(newJson); } private void ScoreUpdates( diff --git a/src/Recyclarr.Cli/Pipelines/ReleaseProfile/Api/Objects/SonarrReleaseProfile.cs b/src/Recyclarr.Cli/Pipelines/ReleaseProfile/Api/Objects/SonarrReleaseProfile.cs index 48c6ca7d..5641325a 100644 --- a/src/Recyclarr.Cli/Pipelines/ReleaseProfile/Api/Objects/SonarrReleaseProfile.cs +++ b/src/Recyclarr.Cli/Pipelines/ReleaseProfile/Api/Objects/SonarrReleaseProfile.cs @@ -1,5 +1,5 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Api.Objects; @@ -12,10 +12,10 @@ public class SonarrPreferredTerm Score = score; } - [JsonProperty("key")] + [JsonPropertyName("key")] public string Term { get; set; } - [JsonProperty("value")] + [JsonPropertyName("value")] public int Score { get; set; } } diff --git a/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs b/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs index 5b718197..a16dbd00 100644 --- a/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs +++ b/src/Recyclarr.Cli/Processors/ErrorHandling/ErrorResponseParser.cs @@ -1,29 +1,30 @@ using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; -using Recyclarr.TrashLib.Json; +using System.Text; +using System.Text.Json; +using Recyclarr.Json; namespace Recyclarr.Cli.Processors.ErrorHandling; public sealed class ErrorResponseParser { private readonly ILogger _log; - private readonly Func _streamFactory; - private readonly JsonSerializer _serializer; + private readonly Func _streamFactory; + private readonly JsonSerializerOptions _jsonSettings; public ErrorResponseParser(ILogger log, string responseBody) { _log = log; - _streamFactory = () => new JsonTextReader(new StringReader(responseBody)); - _serializer = JsonSerializer.Create(GlobalJsonSerializerSettings.Services); + _streamFactory = () => new MemoryStream(Encoding.UTF8.GetBytes(responseBody)); + _jsonSettings = GlobalJsonSerializerSettings.Services; } [SuppressMessage("Design", "CA1031:Do not catch general exception types")] - public bool DeserializeList(Func, IEnumerable> expr) + public bool DeserializeList(Func, IEnumerable> expr) { try { using var stream = _streamFactory(); - var value = _serializer.Deserialize>(stream); + var value = JsonSerializer.Deserialize>(stream, _jsonSettings); if (value is null) { return false; @@ -32,7 +33,7 @@ public sealed class ErrorResponseParser var parsed = expr(value); foreach (var s in parsed) { - _log.Error("Error message from remote service: {Message:l}", (string) s); + _log.Error("Error message from remote service: {Message:l}", s); } return true; @@ -44,18 +45,18 @@ public sealed class ErrorResponseParser } [SuppressMessage("Design", "CA1031:Do not catch general exception types")] - public bool Deserialize(Func expr) + public bool Deserialize(Func expr) { try { using var stream = _streamFactory(); - var value = _serializer.Deserialize(stream); + var value = expr(JsonSerializer.Deserialize(stream, _jsonSettings)); if (value is null) { return false; } - _log.Error("Error message from remote service: {Message:l}", (string) expr(value)); + _log.Error("Error message from remote service: {Message:l}", value); return true; } catch diff --git a/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs b/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs index 294ff1ff..2c79d0bb 100644 --- a/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs +++ b/src/Recyclarr.Cli/Processors/ErrorHandling/FlurlHttpExceptionHandler.cs @@ -34,13 +34,13 @@ public class FlurlHttpExceptionHandler : IFlurlHttpExceptionHandler var parser = new ErrorResponseParser(_log, responseBody); if (parser.DeserializeList(s => s - .Select(x => (string) x.errorMessage) + .Select(x => x.GetProperty("errorMessage").GetString()) .NotNull(x => !string.IsNullOrEmpty(x)))) { return; } - if (parser.Deserialize(s => s.message)) + if (parser.Deserialize(s => s.GetProperty("message").GetString())) { return; } diff --git a/src/Recyclarr.Cli/Recyclarr.Cli.csproj b/src/Recyclarr.Cli/Recyclarr.Cli.csproj index efe62dd7..89f11101 100644 --- a/src/Recyclarr.Cli/Recyclarr.Cli.csproj +++ b/src/Recyclarr.Cli/Recyclarr.Cli.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Recyclarr.Common/Recyclarr.Common.csproj b/src/Recyclarr.Common/Recyclarr.Common.csproj index a1c3cf73..082d4aad 100644 --- a/src/Recyclarr.Common/Recyclarr.Common.csproj +++ b/src/Recyclarr.Common/Recyclarr.Common.csproj @@ -4,7 +4,6 @@ - diff --git a/src/Recyclarr.Json/CollectionJsonConverter.cs b/src/Recyclarr.Json/CollectionJsonConverter.cs new file mode 100644 index 00000000..d4ed9c31 --- /dev/null +++ b/src/Recyclarr.Json/CollectionJsonConverter.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Recyclarr.Json; + +public class CollectionJsonConverter : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return !IsExcludedType(typeToConvert) && + typeToConvert.IsGenericType && + IsAssignableToGenericType(typeToConvert, typeof(IEnumerable<>)); + } + + private static bool IsExcludedType(Type type) + { + return type.IsPrimitive || + type == typeof(string); + } + + private static bool IsAssignableToGenericType(Type givenType, Type genericType) + { + var interfaceTypes = givenType.GetInterfaces() + .Where(x => x.IsGenericType) + .Select(x => x.GetGenericTypeDefinition()); + + return interfaceTypes.Contains(genericType); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var elementType = typeToConvert.GetGenericArguments()[0]; + Type converterType; + + if (typeToConvert.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>)) + { + converterType = typeof(ReadOnlyCollectionJsonConverter<>).MakeGenericType(elementType); + } + else + { + throw new JsonException(); + } + + var instance = Activator.CreateInstance(converterType); + + if (instance is null) + { + throw new JsonException(); + } + + return (JsonConverter) instance; + } +} diff --git a/src/Recyclarr.Json/GlobalJsonSerializerSettings.cs b/src/Recyclarr.Json/GlobalJsonSerializerSettings.cs new file mode 100644 index 00000000..dd16e243 --- /dev/null +++ b/src/Recyclarr.Json/GlobalJsonSerializerSettings.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using JorgeSerrano.Json; + +namespace Recyclarr.Json; + +public static class GlobalJsonSerializerSettings +{ + /// + /// JSON settings used for starr service API payloads. + /// + public static JsonSerializerOptions Services { get; } = new() + { + // This makes sure that null properties, such as maxSize and preferredSize in Radarr + // Quality Definitions, do not get written out to JSON request bodies. + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = {JsonSerializationModifiers.IgnoreNoSerializeAttribute} + } + }; + + /// + /// JSON settings used by cache and other Recyclarr-owned JSON files. + /// + public static JsonSerializerOptions Recyclarr { get; } = new() + { + PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy(), + WriteIndented = true + }; + + /// + /// JSON settings used by Trash Guides JSON files. + /// + public static JsonSerializerOptions Guide { get; } = new() + { + PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy(), + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; +} diff --git a/src/Recyclarr.Json/GlobalUsings.cs b/src/Recyclarr.Json/GlobalUsings.cs new file mode 100644 index 00000000..afb03cf0 --- /dev/null +++ b/src/Recyclarr.Json/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Serilog; +global using JetBrains.Annotations; diff --git a/src/Recyclarr.TrashLib/Json/JsonAutofacModule.cs b/src/Recyclarr.Json/JsonAutofacModule.cs similarity index 80% rename from src/Recyclarr.TrashLib/Json/JsonAutofacModule.cs rename to src/Recyclarr.Json/JsonAutofacModule.cs index 726bcf1d..9b7561c1 100644 --- a/src/Recyclarr.TrashLib/Json/JsonAutofacModule.cs +++ b/src/Recyclarr.Json/JsonAutofacModule.cs @@ -1,14 +1,15 @@ +using System.Text.Json; using Autofac; -using Newtonsoft.Json; +using Recyclarr.Json.Loading; -namespace Recyclarr.TrashLib.Json; +namespace Recyclarr.Json; public class JsonAutofacModule : Module { protected override void Load(ContainerBuilder builder) { base.Load(builder); - builder.Register>(c => + builder.Register>(c => { return settings => new BulkJsonLoader(c.Resolve(), settings); }); diff --git a/src/Recyclarr.Json/JsonExtensions.cs b/src/Recyclarr.Json/JsonExtensions.cs new file mode 100644 index 00000000..4186627f --- /dev/null +++ b/src/Recyclarr.Json/JsonExtensions.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Recyclarr.Json; + +public static class JsonExtensions +{ + public static JsonSerializerOptions CopyOptionsWithout(this JsonSerializerOptions options) + where T : JsonConverter + { + var jsonSerializerOptions = new JsonSerializerOptions(options); + + var jsonConverter = jsonSerializerOptions.Converters.FirstOrDefault(x => x.GetType() == typeof(T)); + if (jsonConverter is not null) + { + jsonSerializerOptions.Converters.Remove(jsonConverter); + } + + return jsonSerializerOptions; + } +} diff --git a/src/Recyclarr.Json/JsonSerializationModifiers.cs b/src/Recyclarr.Json/JsonSerializationModifiers.cs new file mode 100644 index 00000000..a53ef344 --- /dev/null +++ b/src/Recyclarr.Json/JsonSerializationModifiers.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization.Metadata; + +namespace Recyclarr.Json; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class JsonNoSerializeAttribute : Attribute +{ +} + +public static class JsonSerializationModifiers +{ + private static bool HasAttribute(JsonPropertyInfo? prop, IReadOnlyDictionary> allAttrs) + where T : Attribute + { + if (prop is null) + { + return false; + } + + if (!allAttrs.TryGetValue(prop.Name, out var attrs)) + { + return false; + } + + return attrs.Any(x => x.IsAssignableTo(typeof(T))); + } + + public static void IgnoreNoSerializeAttribute(JsonTypeInfo type) + { + var attrs = type.Properties + .Select(x => (Property: x, Attributes: x.PropertyType.GetCustomAttributes(false).Select(y => y.GetType()))) + .Where(x => x.Attributes.Any()) + .ToDictionary(x => x.Property.Name, x => x.Attributes); + + var props = type.Properties; + foreach (var prop in props) + { + prop.ShouldSerialize = (_, _) => !HasAttribute(prop, attrs); + } + } +} diff --git a/src/Recyclarr.Common/JsonUtils.cs b/src/Recyclarr.Json/JsonUtils.cs similarity index 94% rename from src/Recyclarr.Common/JsonUtils.cs rename to src/Recyclarr.Json/JsonUtils.cs index 7109edea..454f0f12 100644 --- a/src/Recyclarr.Common/JsonUtils.cs +++ b/src/Recyclarr.Json/JsonUtils.cs @@ -1,7 +1,7 @@ using System.IO.Abstractions; using Recyclarr.Common.Extensions; -namespace Recyclarr.Common; +namespace Recyclarr.Json; public static class JsonUtils { diff --git a/src/Recyclarr.TrashLib/Json/BulkJsonLoader.cs b/src/Recyclarr.Json/Loading/BulkJsonLoader.cs similarity index 79% rename from src/Recyclarr.TrashLib/Json/BulkJsonLoader.cs rename to src/Recyclarr.Json/Loading/BulkJsonLoader.cs index 41530bed..a3df0060 100644 --- a/src/Recyclarr.TrashLib/Json/BulkJsonLoader.cs +++ b/src/Recyclarr.Json/Loading/BulkJsonLoader.cs @@ -1,19 +1,18 @@ using System.IO.Abstractions; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; -using Newtonsoft.Json; -using Recyclarr.Common; +using System.Text.Json; -namespace Recyclarr.TrashLib.Json; +namespace Recyclarr.Json.Loading; public record LoadedJsonObject(IFileInfo File, T Obj); public class BulkJsonLoader : IBulkJsonLoader { private readonly ILogger _log; - private readonly JsonSerializerSettings _serializerSettings; + private readonly JsonSerializerOptions _serializerSettings; - public BulkJsonLoader(ILogger log, JsonSerializerSettings serializerSettings) + public BulkJsonLoader(ILogger log, JsonSerializerOptions serializerSettings) { _log = log; _serializerSettings = serializerSettings; @@ -35,10 +34,10 @@ public class BulkJsonLoader : IBulkJsonLoader private T ParseJson(string guideData, string fileName) { - var obj = JsonConvert.DeserializeObject(guideData, _serializerSettings); + var obj = JsonSerializer.Deserialize(guideData, _serializerSettings); if (obj is null) { - throw new JsonSerializationException($"Unable to parse JSON at file {fileName}"); + throw new JsonException($"Unable to parse JSON at file {fileName}"); } return obj; diff --git a/src/Recyclarr.TrashLib/Json/GuideJsonLoader.cs b/src/Recyclarr.Json/Loading/GuideJsonLoader.cs similarity index 75% rename from src/Recyclarr.TrashLib/Json/GuideJsonLoader.cs rename to src/Recyclarr.Json/Loading/GuideJsonLoader.cs index 9edb801e..16141a59 100644 --- a/src/Recyclarr.TrashLib/Json/GuideJsonLoader.cs +++ b/src/Recyclarr.Json/Loading/GuideJsonLoader.cs @@ -1,13 +1,13 @@ using System.IO.Abstractions; -using Newtonsoft.Json; +using System.Text.Json; -namespace Recyclarr.TrashLib.Json; +namespace Recyclarr.Json.Loading; public class GuideJsonLoader : IBulkJsonLoader { private readonly IBulkJsonLoader _loader; - public GuideJsonLoader(Func jsonLoaderFactory) + public GuideJsonLoader(Func jsonLoaderFactory) { _loader = jsonLoaderFactory(GlobalJsonSerializerSettings.Guide); } diff --git a/src/Recyclarr.TrashLib/Json/IBulkJsonLoader.cs b/src/Recyclarr.Json/Loading/IBulkJsonLoader.cs similarity index 87% rename from src/Recyclarr.TrashLib/Json/IBulkJsonLoader.cs rename to src/Recyclarr.Json/Loading/IBulkJsonLoader.cs index c83288d3..49226312 100644 --- a/src/Recyclarr.TrashLib/Json/IBulkJsonLoader.cs +++ b/src/Recyclarr.Json/Loading/IBulkJsonLoader.cs @@ -1,6 +1,6 @@ using System.IO.Abstractions; -namespace Recyclarr.TrashLib.Json; +namespace Recyclarr.Json.Loading; public interface IBulkJsonLoader { diff --git a/src/Recyclarr.TrashLib/Json/ServiceJsonLoader.cs b/src/Recyclarr.Json/Loading/ServiceJsonLoader.cs similarity index 75% rename from src/Recyclarr.TrashLib/Json/ServiceJsonLoader.cs rename to src/Recyclarr.Json/Loading/ServiceJsonLoader.cs index 95b0484c..4451c6c3 100644 --- a/src/Recyclarr.TrashLib/Json/ServiceJsonLoader.cs +++ b/src/Recyclarr.Json/Loading/ServiceJsonLoader.cs @@ -1,13 +1,13 @@ using System.IO.Abstractions; -using Newtonsoft.Json; +using System.Text.Json; -namespace Recyclarr.TrashLib.Json; +namespace Recyclarr.Json.Loading; public class ServiceJsonLoader : IBulkJsonLoader { private readonly IBulkJsonLoader _loader; - public ServiceJsonLoader(Func jsonLoaderFactory) + public ServiceJsonLoader(Func jsonLoaderFactory) { _loader = jsonLoaderFactory(GlobalJsonSerializerSettings.Services); } diff --git a/src/Recyclarr.Json/ReadOnlyCollectionJsonConverter.cs b/src/Recyclarr.Json/ReadOnlyCollectionJsonConverter.cs new file mode 100644 index 00000000..60b544e2 --- /dev/null +++ b/src/Recyclarr.Json/ReadOnlyCollectionJsonConverter.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Recyclarr.Json; + +[UsedImplicitly] +public sealed class ReadOnlyCollectionJsonConverter : JsonConverter> +{ + public override IReadOnlyCollection Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + var list = new List(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + var elementValue = JsonSerializer.Deserialize(ref reader, options); + if (elementValue is not null) + { + list.Add(elementValue); + } + } + + return list; + } + + public override void Write( + Utf8JsonWriter writer, + IReadOnlyCollection value, + JsonSerializerOptions options) + { + writer.WriteStartArray(); + foreach (var element in value) + { + JsonSerializer.Serialize(writer, element, options); + } + + writer.WriteEndArray(); + } +} diff --git a/src/Recyclarr.Json/Recyclarr.Json.csproj b/src/Recyclarr.Json/Recyclarr.Json.csproj new file mode 100644 index 00000000..8c874e77 --- /dev/null +++ b/src/Recyclarr.Json/Recyclarr.Json.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/Recyclarr.TrashLib.Guide/ConfigTemplateGuideService.cs b/src/Recyclarr.TrashLib.Guide/ConfigTemplateGuideService.cs index 521d8d76..4f23fe5a 100644 --- a/src/Recyclarr.TrashLib.Guide/ConfigTemplateGuideService.cs +++ b/src/Recyclarr.TrashLib.Guide/ConfigTemplateGuideService.cs @@ -1,9 +1,8 @@ -using System.Collections.ObjectModel; using System.IO.Abstractions; +using System.Text.Json; using JetBrains.Annotations; -using Newtonsoft.Json; +using Recyclarr.Json; using Recyclarr.TrashLib.Config; -using Recyclarr.TrashLib.Json; using Recyclarr.TrashLib.Repo; namespace Recyclarr.TrashLib.Guide; @@ -13,8 +12,8 @@ public record TemplateEntry(string Id, string Template, bool Hidden = false); public record TemplatesData { - public ReadOnlyCollection Radarr { get; [UsedImplicitly] init; } = new(Array.Empty()); - public ReadOnlyCollection Sonarr { get; [UsedImplicitly] init; } = new(Array.Empty()); + public IReadOnlyCollection Radarr { get; [UsedImplicitly] init; } = Array.Empty(); + public IReadOnlyCollection Sonarr { get; [UsedImplicitly] init; } = Array.Empty(); } public record TemplatePath @@ -76,11 +75,8 @@ public class ConfigTemplateGuideService : IConfigTemplateGuideService private static TemplatesData Deserialize(IFileInfo jsonFile) { - var serializer = JsonSerializer.Create(GlobalJsonSerializerSettings.Recyclarr); - - using var stream = new JsonTextReader(jsonFile.OpenText()); - - var obj = serializer.Deserialize(stream); + using var stream = jsonFile.OpenRead(); + var obj = JsonSerializer.Deserialize(stream, GlobalJsonSerializerSettings.Recyclarr); if (obj is null) { throw new InvalidDataException($"Unable to deserialize {jsonFile}"); diff --git a/src/Recyclarr.TrashLib.Guide/CustomFormat/CustomFormatLoader.cs b/src/Recyclarr.TrashLib.Guide/CustomFormat/CustomFormatLoader.cs index bc2ec0c7..4eb31478 100644 --- a/src/Recyclarr.TrashLib.Guide/CustomFormat/CustomFormatLoader.cs +++ b/src/Recyclarr.TrashLib.Guide/CustomFormat/CustomFormatLoader.cs @@ -1,7 +1,7 @@ using System.IO.Abstractions; using System.Reactive.Linq; using Recyclarr.Common.Extensions; -using Recyclarr.TrashLib.Json; +using Recyclarr.Json.Loading; using Recyclarr.TrashLib.Models; namespace Recyclarr.TrashLib.Guide.CustomFormat; diff --git a/src/Recyclarr.TrashLib.Guide/QualitySize/QualitySizeGuideParser.cs b/src/Recyclarr.TrashLib.Guide/QualitySize/QualitySizeGuideParser.cs index 2e23f486..2393f757 100644 --- a/src/Recyclarr.TrashLib.Guide/QualitySize/QualitySizeGuideParser.cs +++ b/src/Recyclarr.TrashLib.Guide/QualitySize/QualitySizeGuideParser.cs @@ -1,9 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using Recyclarr.Common; +using System.Text.Json; using Recyclarr.Common.Extensions; +using Recyclarr.Json; namespace Recyclarr.TrashLib.Guide.QualitySize; @@ -28,21 +27,13 @@ public class QualitySizeGuideParser "Exceptions not rethrown so we can continue processing other files")] private QualitySizeData? ParseQuality(IFileInfo jsonFile) { - var serializer = JsonSerializer.Create(new JsonSerializerSettings - { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy() - } - }); - QualitySizeData? quality = null; Exception? exception = null; - using var json = new JsonTextReader(jsonFile.OpenText()); try { - quality = serializer.Deserialize(json); + using var stream = jsonFile.OpenRead(); + quality = JsonSerializer.Deserialize(stream, GlobalJsonSerializerSettings.Guide); } catch (Exception e) { diff --git a/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileData.cs b/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileData.cs index 1389e893..fbcfbfbd 100644 --- a/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileData.cs +++ b/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileData.cs @@ -1,12 +1,12 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; namespace Recyclarr.TrashLib.Guide.ReleaseProfile; [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public record TermData { - [JsonProperty("trash_id")] + [JsonPropertyName("trash_id")] public string TrashId { get; init; } = string.Empty; public string Name { get; init; } = string.Empty; @@ -39,7 +39,7 @@ public record PreferredTermData [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public record ReleaseProfileData { - [JsonProperty("trash_id")] + [JsonPropertyName("trash_id")] public string TrashId { get; init; } = string.Empty; public string Name { get; init; } = string.Empty; diff --git a/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileGuideParser.cs b/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileGuideParser.cs index f7246bff..0d5b51b6 100644 --- a/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileGuideParser.cs +++ b/src/Recyclarr.TrashLib.Guide/ReleaseProfile/ReleaseProfileGuideParser.cs @@ -1,25 +1,33 @@ using System.IO.Abstractions; -using Newtonsoft.Json; -using Recyclarr.Common; +using System.Text.Json; +using Recyclarr.Json; namespace Recyclarr.TrashLib.Guide.ReleaseProfile; public class ReleaseProfileGuideParser { private readonly ILogger _log; + private readonly JsonSerializerOptions _jsonSettings; public ReleaseProfileGuideParser(ILogger log) { _log = log; + _jsonSettings = new JsonSerializerOptions(GlobalJsonSerializerSettings.Services) + { + Converters = + { + new CollectionJsonConverter(), + new TermDataConverter() + } + }; } - private async Task LoadAndParseFile(IFileInfo file, params JsonConverter[] converters) + private async Task LoadAndParseFile(IFileInfo file) { try { - using var stream = file.OpenText(); - var json = await stream.ReadToEndAsync(); - return JsonConvert.DeserializeObject(json, converters); + await using var stream = file.OpenRead(); + return await JsonSerializer.DeserializeAsync(stream, _jsonSettings); } catch (JsonException e) { @@ -42,10 +50,7 @@ public class ReleaseProfileGuideParser public IEnumerable GetReleaseProfileData(IEnumerable paths) { - var converter = new TermDataConverter(); - var tasks = JsonUtils.GetJsonFilesInDirectories(paths, _log) - .Select(x => LoadAndParseFile(x, converter)); - + var tasks = JsonUtils.GetJsonFilesInDirectories(paths, _log).Select(LoadAndParseFile); var data = Task.WhenAll(tasks).Result // Make non-nullable type and filter out null values .Choose(x => x is not null ? (true, x) : default); diff --git a/src/Recyclarr.TrashLib.Guide/ReleaseProfile/TermDataConverter.cs b/src/Recyclarr.TrashLib.Guide/ReleaseProfile/TermDataConverter.cs index 7290290e..68fa905d 100644 --- a/src/Recyclarr.TrashLib.Guide/ReleaseProfile/TermDataConverter.cs +++ b/src/Recyclarr.TrashLib.Guide/ReleaseProfile/TermDataConverter.cs @@ -1,33 +1,24 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Recyclarr.Json; namespace Recyclarr.TrashLib.Guide.ReleaseProfile; -internal class TermDataConverter : JsonConverter +public class TermDataConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override TermData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - // Not to be used for serialization - throw new NotImplementedException(); - } - - public override object? ReadJson( - JsonReader reader, - Type objectType, - object? existingValue, - JsonSerializer serializer) - { - var token = JToken.Load(reader); - return token.Type switch + if (reader.TokenType is JsonTokenType.String) { - JTokenType.Object => token.ToObject(), - JTokenType.String => new TermData {Term = token.ToString()}, - _ => null - }; + var str = reader.GetString(); + return str is not null ? new TermData {Term = str} : null; + } + + return JsonSerializer.Deserialize(ref reader, options.CopyOptionsWithout()); } - public override bool CanConvert(Type objectType) + public override void Write(Utf8JsonWriter writer, TermData value, JsonSerializerOptions options) { - return objectType == typeof(TermData); + JsonSerializer.Serialize(writer, value, options.CopyOptionsWithout()); } } diff --git a/src/Recyclarr.TrashLib/Http/FlurlClientFactory.cs b/src/Recyclarr.TrashLib/Http/FlurlClientFactory.cs index f8d00354..b538e17a 100644 --- a/src/Recyclarr.TrashLib/Http/FlurlClientFactory.cs +++ b/src/Recyclarr.TrashLib/Http/FlurlClientFactory.cs @@ -1,7 +1,7 @@ using Flurl.Http; using Flurl.Http.Configuration; using Recyclarr.Common.Networking; -using Recyclarr.TrashLib.Json; +using Recyclarr.Json; using Recyclarr.TrashLib.Settings; namespace Recyclarr.TrashLib.Http; @@ -30,7 +30,7 @@ public class FlurlClientFactory : IFlurlClientFactory { var settings = new ClientFlurlHttpSettings { - JsonSerializer = new NewtonsoftJsonSerializer(GlobalJsonSerializerSettings.Services) + JsonSerializer = new DefaultJsonSerializer(GlobalJsonSerializerSettings.Services) }; FlurlLogging.SetupLogging(settings, _log); diff --git a/src/Recyclarr.TrashLib/Http/FlurlLogging.cs b/src/Recyclarr.TrashLib/Http/FlurlLogging.cs index 37e6b0fd..4ff8b8ab 100644 --- a/src/Recyclarr.TrashLib/Http/FlurlLogging.cs +++ b/src/Recyclarr.TrashLib/Http/FlurlLogging.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using Flurl; using Flurl.Http.Configuration; -using Newtonsoft.Json; namespace Recyclarr.TrashLib.Http; @@ -51,7 +51,7 @@ public static class FlurlLogging try { - body = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(body)); + body = JsonSerializer.Serialize(JsonSerializer.Deserialize(body)); } catch (JsonException) { diff --git a/src/Recyclarr.TrashLib/Json/GlobalJsonSerializerSettings.cs b/src/Recyclarr.TrashLib/Json/GlobalJsonSerializerSettings.cs deleted file mode 100644 index d2863419..00000000 --- a/src/Recyclarr.TrashLib/Json/GlobalJsonSerializerSettings.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Recyclarr.TrashLib.Json; - -public static class GlobalJsonSerializerSettings -{ - /// - /// JSON settings used for starr service API payloads. - /// - public static JsonSerializerSettings Services { get; } = new() - { - // This makes sure that null properties, such as maxSize and preferredSize in Radarr - // Quality Definitions, do not get written out to JSON request bodies. - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new ServiceContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy() - } - }; - - /// - /// JSON settings used by cache and other Recyclarr-owned JSON files. - /// - public static JsonSerializerSettings Recyclarr { get; } = new() - { - Formatting = Formatting.Indented, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy() - } - }; - - /// - /// JSON settings used by Trash Guides JSON files. - /// - public static JsonSerializerSettings Guide => Recyclarr; -} diff --git a/src/Recyclarr.TrashLib/Json/ServiceContractResolver.cs b/src/Recyclarr.TrashLib/Json/ServiceContractResolver.cs deleted file mode 100644 index 675ec565..00000000 --- a/src/Recyclarr.TrashLib/Json/ServiceContractResolver.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Recyclarr.TrashLib.Json; - -[AttributeUsage(AttributeTargets.Property)] -public sealed class JsonNoSerializeAttribute : Attribute -{ -} - -[AttributeUsage(AttributeTargets.Property)] -public sealed class JsonNoDeserializeAttribute : Attribute -{ -} - -public class ServiceContractResolver : DefaultContractResolver -{ - private static bool HasAttribute(JsonProperty prop, Dictionary> allAttrs) - where T : Attribute - { - var name = prop.UnderlyingName; - if (name is null) - { - return false; - } - - if (!allAttrs.TryGetValue(name, out var attrs)) - { - return false; - } - - return attrs.Any(x => x.IsAssignableTo(typeof(T))); - } - - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) - { - var attrs = type.GetProperties() - .Select(x => (Property: x, Attributes: x.GetCustomAttributes(false).Select(y => y.GetType()))) - .Where(x => x.Attributes.Any()) - .ToDictionary(x => x.Property.Name, x => x.Attributes); - - var props = base.CreateProperties(type, memberSerialization); - foreach (var prop in props) - { - prop.ShouldSerialize = _ => !HasAttribute(prop, attrs); - prop.ShouldDeserialize = _ => !HasAttribute(prop, attrs); - } - - return props; - } -} diff --git a/src/Recyclarr.TrashLib/Models/CustomFormatData.cs b/src/Recyclarr.TrashLib/Models/CustomFormatData.cs index a873bb1f..0beefeee 100644 --- a/src/Recyclarr.TrashLib/Models/CustomFormatData.cs +++ b/src/Recyclarr.TrashLib/Models/CustomFormatData.cs @@ -1,12 +1,14 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Recyclarr.Common.Extensions; -using Recyclarr.TrashLib.Json; +using Recyclarr.Json; namespace Recyclarr.TrashLib.Models; public record CustomFormatFieldData { public string Name { get; } = nameof(Value).ToCamelCase(); + + [JsonConverter(typeof(FieldValueConverter))] public object? Value { get; init; } } @@ -28,11 +30,11 @@ public record CustomFormatData [JsonIgnore] public string? Category { get; init; } - [JsonProperty("trash_id")] + [JsonPropertyName("trash_id")] [JsonNoSerialize] public string TrashId { get; init; } = ""; - [JsonProperty("trash_scores")] + [JsonPropertyName("trash_scores")] [JsonNoSerialize] public Dictionary TrashScores { get; init; } = new(StringComparer.InvariantCultureIgnoreCase); diff --git a/src/Recyclarr.TrashLib/Models/FieldValueConverter.cs b/src/Recyclarr.TrashLib/Models/FieldValueConverter.cs new file mode 100644 index 00000000..f8a6db29 --- /dev/null +++ b/src/Recyclarr.TrashLib/Models/FieldValueConverter.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Recyclarr.TrashLib.Models; + +public class FieldValueConverter : JsonConverter +{ + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => reader.GetInt32(), + JsonTokenType.String => reader.GetString(), + _ => throw new JsonException($"CF field of type {reader.TokenType} is not supported") + }; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } +} diff --git a/src/Recyclarr.TrashLib/Models/FieldsArrayJsonConverter.cs b/src/Recyclarr.TrashLib/Models/FieldsArrayJsonConverter.cs index ef8e47f3..0c738e0c 100644 --- a/src/Recyclarr.TrashLib/Models/FieldsArrayJsonConverter.cs +++ b/src/Recyclarr.TrashLib/Models/FieldsArrayJsonConverter.cs @@ -1,34 +1,29 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Recyclarr.TrashLib.Models; -public class FieldsArrayJsonConverter : JsonConverter +public class FieldsArrayJsonConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override bool CanConvert(Type typeToConvert) { - serializer.Serialize(writer, value); + return typeToConvert == typeof(CustomFormatFieldData) || + Array.Exists(typeToConvert.GetInterfaces(), + x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>)); } - public override object? ReadJson( - JsonReader reader, - Type objectType, - object? existingValue, - JsonSerializer serializer) + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var token = JToken.Load(reader); - - // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault - return token.Type switch + if (reader.TokenType is JsonTokenType.StartObject) { - JTokenType.Object => new[] {token.ToObject()}, - JTokenType.Array => token.ToObject(), - _ => throw new InvalidOperationException("Unsupported token type for CustomFormatFieldData") - }; + return new[] {JsonSerializer.Deserialize(ref reader, options)!}; + } + + return JsonSerializer.Deserialize(ref reader, options)!; } - public override bool CanConvert(Type objectType) + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { - return objectType.IsArray; + JsonSerializer.Serialize(writer, value, options); } } diff --git a/src/Recyclarr.TrashLib/Recyclarr.TrashLib.csproj b/src/Recyclarr.TrashLib/Recyclarr.TrashLib.csproj index c08b7b26..2662d0bf 100644 --- a/src/Recyclarr.TrashLib/Recyclarr.TrashLib.csproj +++ b/src/Recyclarr.TrashLib/Recyclarr.TrashLib.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Recyclarr.TrashLib/Repo/TrashRepoMetadataBuilder.cs b/src/Recyclarr.TrashLib/Repo/TrashRepoMetadataBuilder.cs index c1a486c2..0973c1cc 100644 --- a/src/Recyclarr.TrashLib/Repo/TrashRepoMetadataBuilder.cs +++ b/src/Recyclarr.TrashLib/Repo/TrashRepoMetadataBuilder.cs @@ -1,6 +1,6 @@ using System.IO.Abstractions; -using Newtonsoft.Json; -using Recyclarr.TrashLib.Json; +using System.Text.Json; +using Recyclarr.Json; namespace Recyclarr.TrashLib.Repo; @@ -17,11 +17,9 @@ public class TrashRepoMetadataBuilder : IRepoMetadataBuilder private static RepoMetadata Deserialize(IFileInfo jsonFile) { - var serializer = JsonSerializer.Create(GlobalJsonSerializerSettings.Guide); + using var stream = jsonFile.OpenRead(); - using var stream = new JsonTextReader(jsonFile.OpenText()); - - var obj = serializer.Deserialize(stream); + var obj = JsonSerializer.Deserialize(stream, GlobalJsonSerializerSettings.Guide); if (obj is null) { throw new InvalidDataException($"Unable to deserialize {jsonFile}"); diff --git a/src/Recyclarr.TrashLib/TrashLibAutofacModule.cs b/src/Recyclarr.TrashLib/TrashLibAutofacModule.cs index cda6f9b3..954201d0 100644 --- a/src/Recyclarr.TrashLib/TrashLibAutofacModule.cs +++ b/src/Recyclarr.TrashLib/TrashLibAutofacModule.cs @@ -2,10 +2,10 @@ using Autofac; using Autofac.Extras.Ordering; using Recyclarr.Common; using Recyclarr.Common.FluentValidation; +using Recyclarr.Json; using Recyclarr.TrashLib.ApiServices; using Recyclarr.TrashLib.Compatibility; using Recyclarr.TrashLib.Http; -using Recyclarr.TrashLib.Json; using Recyclarr.TrashLib.Repo; using Recyclarr.TrashLib.Repo.VersionControl; using Recyclarr.TrashLib.Settings; diff --git a/src/Recyclarr.sln b/src/Recyclarr.sln index f37b1aea..12bcf3e0 100644 --- a/src/Recyclarr.sln +++ b/src/Recyclarr.sln @@ -54,6 +54,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.TrashLib.Config.T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.TrashLib.Guide.TestLibrary", "tests\Recyclarr.TrashLib.Guide.TestLibrary\Recyclarr.TrashLib.Guide.TestLibrary.csproj", "{B3FC265C-36EF-4815-9EB1-CB878FBEE6D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.Json", "Recyclarr.Json\Recyclarr.Json.csproj", "{A9E2F11E-73F8-48CC-8770-0AFD41E80141}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.Json.Tests", "tests\Recyclarr.Json.Tests\Recyclarr.Json.Tests.csproj", "{737A1B22-F3A5-4920-AED7-77E756D14113}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.Json.TestLibrary", "tests\Recyclarr.Json.TestLibrary\Recyclarr.Json.TestLibrary.csproj", "{59B75C08-F144-4190-A7D0-C027044147D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -136,6 +142,18 @@ Global {B3FC265C-36EF-4815-9EB1-CB878FBEE6D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3FC265C-36EF-4815-9EB1-CB878FBEE6D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {B3FC265C-36EF-4815-9EB1-CB878FBEE6D6}.Release|Any CPU.Build.0 = Release|Any CPU + {A9E2F11E-73F8-48CC-8770-0AFD41E80141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9E2F11E-73F8-48CC-8770-0AFD41E80141}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9E2F11E-73F8-48CC-8770-0AFD41E80141}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9E2F11E-73F8-48CC-8770-0AFD41E80141}.Release|Any CPU.Build.0 = Release|Any CPU + {737A1B22-F3A5-4920-AED7-77E756D14113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {737A1B22-F3A5-4920-AED7-77E756D14113}.Debug|Any CPU.Build.0 = Debug|Any CPU + {737A1B22-F3A5-4920-AED7-77E756D14113}.Release|Any CPU.ActiveCfg = Release|Any CPU + {737A1B22-F3A5-4920-AED7-77E756D14113}.Release|Any CPU.Build.0 = Release|Any CPU + {59B75C08-F144-4190-A7D0-C027044147D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59B75C08-F144-4190-A7D0-C027044147D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59B75C08-F144-4190-A7D0-C027044147D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59B75C08-F144-4190-A7D0-C027044147D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -156,5 +174,7 @@ Global {2B4CAFB7-02D7-4909-98C4-7393C1366C39} = {18E17C53-F600-40AE-82C1-3CD1E547C307} {533299C5-71A9-4461-90BB-05C3404F2143} = {18E17C53-F600-40AE-82C1-3CD1E547C307} {B3FC265C-36EF-4815-9EB1-CB878FBEE6D6} = {18E17C53-F600-40AE-82C1-3CD1E547C307} + {737A1B22-F3A5-4920-AED7-77E756D14113} = {18E17C53-F600-40AE-82C1-3CD1E547C307} + {59B75C08-F144-4190-A7D0-C027044147D2} = {18E17C53-F600-40AE-82C1-3CD1E547C307} EndGlobalSection EndGlobal diff --git a/src/tests/Recyclarr.Cli.Tests/Cache/ServiceCacheTest.cs b/src/tests/Recyclarr.Cli.Tests/Cache/ServiceCacheTest.cs index 3c8cceb8..66039cce 100644 --- a/src/tests/Recyclarr.Cli.Tests/Cache/ServiceCacheTest.cs +++ b/src/tests/Recyclarr.Cli.Tests/Cache/ServiceCacheTest.cs @@ -46,7 +46,10 @@ public class ServiceCacheTest IServiceConfiguration config, ServiceCache sut) { - const string testJson = @"{'test_value': 'Foo'}"; + const string testJson = + """ + {"test_value": "Foo"} + """; const string testJsonPath = "cacheFile.json"; fs.AddFile(testJsonPath, new MockFileData(testJson)); @@ -193,12 +196,12 @@ public class ServiceCacheTest const string cacheJson = """ { - 'version': 1, - 'trash_id_mappings': [ + "version": 1, + "trash_id_mappings": [ { - 'custom_format_name': '4K Remaster', - 'trash_id': 'eca37840c13c6ef2dd0262b141a5482f', - 'custom_format_id': 4 + "custom_format_name": "4K Remaster", + "trash_id": "eca37840c13c6ef2dd0262b141a5482f", + "custom_format_id": 4 } ] } diff --git a/src/tests/Recyclarr.Cli.Tests/Pipelines/CustomFormat/Models/FieldsArrayJsonConverterTest.cs b/src/tests/Recyclarr.Cli.Tests/Pipelines/CustomFormat/Models/FieldsArrayJsonConverterTest.cs index 567e5fdb..67c4e656 100644 --- a/src/tests/Recyclarr.Cli.Tests/Pipelines/CustomFormat/Models/FieldsArrayJsonConverterTest.cs +++ b/src/tests/Recyclarr.Cli.Tests/Pipelines/CustomFormat/Models/FieldsArrayJsonConverterTest.cs @@ -1,5 +1,5 @@ -using Flurl.Http.Configuration; -using Recyclarr.TrashLib.Json; +using System.Text.Json; +using Recyclarr.Json; using Recyclarr.TrashLib.Models; namespace Recyclarr.Cli.Tests.Pipelines.CustomFormat.Models; @@ -11,8 +11,6 @@ public class FieldsArrayJsonConverterTest [Test] public void Read_multiple_as_array() { - var serializer = new NewtonsoftJsonSerializer(GlobalJsonSerializerSettings.Services); - const string json = """ { @@ -40,26 +38,20 @@ public class FieldsArrayJsonConverterTest ] } """; - var result = serializer.Deserialize(json); - result.Fields.Should().BeEquivalentTo(new[] + var result = + JsonSerializer.Deserialize(json, GlobalJsonSerializerSettings.Services); + + result!.Fields.Should().BeEquivalentTo(new[] { - new CustomFormatFieldData - { - Value = 25 - }, - new CustomFormatFieldData - { - Value = 40 - } + new CustomFormatFieldData {Value = 25}, + new CustomFormatFieldData {Value = 40} }); } [Test] public void Read_single_as_array() { - var serializer = new NewtonsoftJsonSerializer(GlobalJsonSerializerSettings.Services); - const string json = """ { @@ -69,36 +61,34 @@ public class FieldsArrayJsonConverterTest "label": "Minimum Size", "unit": "GB", "helpText": "Release must be greater than this size", - "value": 25, + "value": "25", "type": "number", "advanced": false } } """; - var result = serializer.Deserialize(json); + var result = + JsonSerializer.Deserialize(json, GlobalJsonSerializerSettings.Services); - result.Fields.Should().BeEquivalentTo(new[] + result!.Fields.Should().BeEquivalentTo(new[] { - new CustomFormatFieldData - { - Value = 25 - } + new CustomFormatFieldData {Value = "25"} }); } [Test] public void Read_throws_on_unsupported_token_type() { - var serializer = new NewtonsoftJsonSerializer(GlobalJsonSerializerSettings.Services); - const string json = """ { "fields": 0 } """; - var act = () => serializer.Deserialize(json); - act.Should().Throw(); + var act = () => JsonSerializer.Deserialize( + json, GlobalJsonSerializerSettings.Services); + + act.Should().Throw(); } } diff --git a/src/tests/Recyclarr.Json.TestLibrary/JsonIntegrationFixture.cs b/src/tests/Recyclarr.Json.TestLibrary/JsonIntegrationFixture.cs new file mode 100644 index 00000000..486f2347 --- /dev/null +++ b/src/tests/Recyclarr.Json.TestLibrary/JsonIntegrationFixture.cs @@ -0,0 +1,14 @@ +using Autofac; +using Recyclarr.TestLibrary; + +namespace Recyclarr.Json.TestLibrary; + +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public abstract class JsonIntegrationFixture : IntegrationTestFixture +{ + protected override void RegisterTypes(ContainerBuilder builder) + { + base.RegisterTypes(builder); + builder.RegisterModule(); + } +} diff --git a/src/tests/Recyclarr.Json.TestLibrary/Recyclarr.Json.TestLibrary.csproj b/src/tests/Recyclarr.Json.TestLibrary/Recyclarr.Json.TestLibrary.csproj new file mode 100644 index 00000000..1573e5c7 --- /dev/null +++ b/src/tests/Recyclarr.Json.TestLibrary/Recyclarr.Json.TestLibrary.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/src/tests/Recyclarr.TrashLib.Tests/Json/BulkJsonLoaderIntegrationTest.cs b/src/tests/Recyclarr.Json.Tests/BulkJsonLoaderIntegrationTest.cs similarity index 60% rename from src/tests/Recyclarr.TrashLib.Tests/Json/BulkJsonLoaderIntegrationTest.cs rename to src/tests/Recyclarr.Json.Tests/BulkJsonLoaderIntegrationTest.cs index 7f27404b..8edac392 100644 --- a/src/tests/Recyclarr.TrashLib.Tests/Json/BulkJsonLoaderIntegrationTest.cs +++ b/src/tests/Recyclarr.Json.Tests/BulkJsonLoaderIntegrationTest.cs @@ -1,17 +1,19 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; -using Recyclarr.TrashLib.Json; -using Recyclarr.TrashLib.Models; -using Recyclarr.TrashLib.TestLibrary; +using Recyclarr.Json.Loading; +using Recyclarr.Json.TestLibrary; -namespace Recyclarr.TrashLib.Tests.Json; +namespace Recyclarr.Json.Tests; [TestFixture] [Parallelizable(ParallelScope.All)] -public class BulkJsonLoaderIntegrationTest : TrashLibIntegrationFixture +public class BulkJsonLoaderIntegrationTest : JsonIntegrationFixture { [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")] - private sealed record TestJsonObject(string TrashId, int TrashScore, string Name); + private sealed record TestGuideObject(string TrashId, int TrashScore, string Name); + + [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")] + private sealed record TestServiceObject(int Id, string Name, bool IncludeCustomFormatWhenRenaming); [Test] public void Guide_deserialize_works() @@ -29,11 +31,11 @@ public class BulkJsonLoaderIntegrationTest : TrashLibIntegrationFixture Fs.AddFile(Fs.CurrentDirectory().File("file.json"), new MockFileData(jsonData)); - var result = sut.LoadAllFilesAtPaths(new[] {Fs.CurrentDirectory()}); + var result = sut.LoadAllFilesAtPaths(new[] {Fs.CurrentDirectory()}); result.Should().BeEquivalentTo(new[] { - new TestJsonObject("90cedc1fea7ea5d11298bebd3d1d3223", -10000, "TheName") + new TestGuideObject("90cedc1fea7ea5d11298bebd3d1d3223", -10000, "TheName") }); } @@ -53,16 +55,11 @@ public class BulkJsonLoaderIntegrationTest : TrashLibIntegrationFixture Fs.AddFile(Fs.CurrentDirectory().File("file.json"), new MockFileData(jsonData)); - var result = sut.LoadAllFilesAtPaths(new[] {Fs.CurrentDirectory()}); + var result = sut.LoadAllFilesAtPaths(new[] {Fs.CurrentDirectory()}); result.Should().BeEquivalentTo(new[] { - new CustomFormatData - { - Id = 22, - Name = "FUNi", - IncludeCustomFormatWhenRenaming = true - } + new TestServiceObject(22, "FUNi", true) }); } } diff --git a/src/tests/Recyclarr.Common.Tests/JsonUtilsTest.cs b/src/tests/Recyclarr.Json.Tests/JsonUtilsTest.cs similarity index 98% rename from src/tests/Recyclarr.Common.Tests/JsonUtilsTest.cs rename to src/tests/Recyclarr.Json.Tests/JsonUtilsTest.cs index 01933367..9ff32ecd 100644 --- a/src/tests/Recyclarr.Common.Tests/JsonUtilsTest.cs +++ b/src/tests/Recyclarr.Json.Tests/JsonUtilsTest.cs @@ -1,7 +1,7 @@ using System.IO.Abstractions; using Recyclarr.TestLibrary; -namespace Recyclarr.Common.Tests; +namespace Recyclarr.Json.Tests; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/src/tests/Recyclarr.Json.Tests/Recyclarr.Json.Tests.csproj b/src/tests/Recyclarr.Json.Tests/Recyclarr.Json.Tests.csproj new file mode 100644 index 00000000..6f3060ab --- /dev/null +++ b/src/tests/Recyclarr.Json.Tests/Recyclarr.Json.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/tests/Recyclarr.TestLibrary/FluentAssertions/JsonEquivalencyStep.cs b/src/tests/Recyclarr.TestLibrary/FluentAssertions/JsonEquivalencyStep.cs deleted file mode 100644 index 2b1e1f5d..00000000 --- a/src/tests/Recyclarr.TestLibrary/FluentAssertions/JsonEquivalencyStep.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentAssertions.Equivalency; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; - -namespace Recyclarr.TestLibrary.FluentAssertions; - -public class JsonEquivalencyStep : IEquivalencyStep -{ - public EquivalencyResult Handle( - Comparands comparands, - IEquivalencyValidationContext context, - IEquivalencyValidator nestedValidator) - { - var canHandle = comparands.Subject?.GetType().IsAssignableTo(typeof(JToken)) ?? false; - if (!canHandle) - { - return EquivalencyResult.ContinueWithNext; - } - - ((JToken) comparands.Subject!).Should().BeEquivalentTo( - (JToken) comparands.Expectation, - context.Reason.FormattedMessage, - context.Reason.Arguments); - - return EquivalencyResult.AssertionCompleted; - } -} diff --git a/src/tests/Recyclarr.TestLibrary/IntegrationTestFixture.cs b/src/tests/Recyclarr.TestLibrary/IntegrationTestFixture.cs new file mode 100644 index 00000000..a0df24ee --- /dev/null +++ b/src/tests/Recyclarr.TestLibrary/IntegrationTestFixture.cs @@ -0,0 +1,86 @@ +using System.IO.Abstractions; +using Autofac; +using Autofac.Features.ResolveAnything; +using Recyclarr.Common; +using Recyclarr.TestLibrary.Autofac; +using Spectre.Console; +using Spectre.Console.Testing; + +namespace Recyclarr.TestLibrary; + +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public abstract class IntegrationTestFixture : IDisposable +{ + private readonly Lazy _container; + protected ILifetimeScope Container => _container.Value; + protected MockFileSystem Fs { get; } + protected TestConsole Console { get; } = new(); + protected TestableLogger Logger { get; } = new(); + + protected IntegrationTestFixture() + { + Fs = new MockFileSystem(new MockFileSystemOptions + { + CreateDefaultTempDir = false + }); + + // Use Lazy because we shouldn't invoke virtual methods at construction time + _container = new Lazy(() => + { + var builder = new ContainerBuilder(); + RegisterTypes(builder); + RegisterStubsAndMocks(builder); + builder.RegisterSource(); + return builder.Build(); + }); + } + + /// + /// Register "real" types (usually Module-derived classes from other projects). This call happens + /// before + /// RegisterStubsAndMocks(). + /// + protected virtual void RegisterTypes(ContainerBuilder builder) + { + } + + /// + /// Override registrations made in the RegisterTypes() method. This method is called after + /// RegisterTypes(). + /// + protected virtual void RegisterStubsAndMocks(ContainerBuilder builder) + { + builder.RegisterInstance(Fs).As(); + builder.RegisterInstance(Console).As(); + builder.RegisterInstance(Logger).As(); + + builder.RegisterMockFor(); + } + + protected T Resolve() where T : notnull + { + return Container.Resolve(); + } + + // ReSharper disable once VirtualMemberNeverOverridden.Global + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + if (!_container.IsValueCreated) + { + return; + } + + _container.Value.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/tests/Recyclarr.TestLibrary/MockData.cs b/src/tests/Recyclarr.TestLibrary/MockData.cs index 85da2470..e1fbb2c6 100644 --- a/src/tests/Recyclarr.TestLibrary/MockData.cs +++ b/src/tests/Recyclarr.TestLibrary/MockData.cs @@ -1,12 +1,12 @@ -using Newtonsoft.Json; +using System.Text.Json; namespace Recyclarr.TestLibrary; public static class MockData { - public static MockFileData FromJson(object json) + public static MockFileData FromJson(object json, JsonSerializerOptions options) { - return new MockFileData(JsonConvert.SerializeObject(json)); + return new MockFileData(JsonSerializer.Serialize(json, options)); } public static MockFileData FromString(string data) diff --git a/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/ConfigurationLoaderTest.cs b/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/ConfigurationLoaderTest.cs index 85a48407..16d84c0e 100644 --- a/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/ConfigurationLoaderTest.cs +++ b/src/tests/Recyclarr.TrashLib.Config.Tests/Parsing/ConfigurationLoaderTest.cs @@ -21,9 +21,9 @@ public class ConfigurationLoaderTest : ConfigIntegrationFixture return () => new StringReader(testData.ReadData(file)); } - protected override void RegisterTypes(ContainerBuilder builder) + protected override void RegisterStubsAndMocks(ContainerBuilder builder) { - base.RegisterTypes(builder); + base.RegisterStubsAndMocks(builder); builder.RegisterMockFor>(); builder.RegisterMockFor>(); } diff --git a/src/tests/Recyclarr.TrashLib.Guide.Tests/CustomFormat/CustomFormatLoaderIntegrationTest.cs b/src/tests/Recyclarr.TrashLib.Guide.Tests/CustomFormat/CustomFormatLoaderIntegrationTest.cs index 104a719b..4badbcfe 100644 --- a/src/tests/Recyclarr.TrashLib.Guide.Tests/CustomFormat/CustomFormatLoaderIntegrationTest.cs +++ b/src/tests/Recyclarr.TrashLib.Guide.Tests/CustomFormat/CustomFormatLoaderIntegrationTest.cs @@ -1,5 +1,5 @@ using System.IO.Abstractions; -using Newtonsoft.Json.Linq; +using System.Text.Json; using Recyclarr.TrashLib.Guide.CustomFormat; using Recyclarr.TrashLib.Guide.TestLibrary; using Recyclarr.TrashLib.TestLibrary; @@ -14,8 +14,8 @@ public class CustomFormatLoaderIntegrationTest : GuideIntegrationFixture public void Get_custom_format_json_works() { var sut = Resolve(); - Fs.AddFile("first.json", new MockFileData("{'name':'first','trash_id':'1'}")); - Fs.AddFile("second.json", new MockFileData("{'name':'second','trash_id':'2'}")); + Fs.AddFile("first.json", new MockFileData("""{"name":"first","trash_id":"1"}""")); + Fs.AddFile("second.json", new MockFileData("""{"name":"second","trash_id":"2"}""")); Fs.AddFile("collection_of_cfs.md", new MockFileData("")); var dir = Fs.CurrentDirectory(); @@ -25,6 +25,6 @@ public class CustomFormatLoaderIntegrationTest : GuideIntegrationFixture { NewCf.Data("first", "1"), NewCf.Data("second", "2") - }, o => o.Excluding(x => x.Type == typeof(JObject))); + }, o => o.Excluding(x => x.Type == typeof(JsonElement))); } } diff --git a/src/tests/Recyclarr.TrashLib.Guide.Tests/ReleaseProfile/ReleaseProfileGuideServiceTest.cs b/src/tests/Recyclarr.TrashLib.Guide.Tests/ReleaseProfile/ReleaseProfileGuideServiceTest.cs index 2aac9ff1..f30201ae 100644 --- a/src/tests/Recyclarr.TrashLib.Guide.Tests/ReleaseProfile/ReleaseProfileGuideServiceTest.cs +++ b/src/tests/Recyclarr.TrashLib.Guide.Tests/ReleaseProfile/ReleaseProfileGuideServiceTest.cs @@ -1,5 +1,5 @@ using System.IO.Abstractions; -using Newtonsoft.Json; +using Recyclarr.Json; using Recyclarr.TestLibrary; using Recyclarr.TestLibrary.AutoFixture; using Recyclarr.TrashLib.Guide.ReleaseProfile; @@ -30,18 +30,16 @@ public class ReleaseProfileGuideServiceTest }; } - static MockFileData MockFileData(dynamic obj) - { - return new MockFileData(JsonConvert.SerializeObject(obj)); - } - var mockData1 = MakeMockObject("first"); var mockData2 = MakeMockObject("second"); var baseDir = fs.CurrentDirectory().SubDirectory("files"); baseDir.Create(); - fs.AddFile(baseDir.File("first.json").FullName, MockFileData(mockData1)); - fs.AddFile(baseDir.File("second.json").FullName, MockFileData(mockData2)); + fs.AddFile(baseDir.File("first.json").FullName, + MockData.FromJson(mockData1, GlobalJsonSerializerSettings.Services)); + + fs.AddFile(baseDir.File("second.json").FullName, + MockData.FromJson(mockData2, GlobalJsonSerializerSettings.Services)); metadataBuilder.ToDirectoryInfoList(default!).ReturnsForAnyArgs(new[] {baseDir}); @@ -74,8 +72,11 @@ public class ReleaseProfileGuideServiceTest } }; - fs.AddFile(rootPath.File("0_bad_data.json").FullName, MockData.FromString(badData)); - fs.AddFile(rootPath.File("1_good_data.json").FullName, MockData.FromJson(goodData)); + fs.AddFile(rootPath.File("0_bad_data.json").FullName, + MockData.FromString(badData)); + + fs.AddFile(rootPath.File("1_good_data.json").FullName, + MockData.FromJson(goodData, GlobalJsonSerializerSettings.Services)); metadataBuilder.ToDirectoryInfoList(default!).ReturnsForAnyArgs(new[] {rootPath}); diff --git a/src/tests/Recyclarr.TrashLib.TestLibrary/TrashLibIntegrationFixture.cs b/src/tests/Recyclarr.TrashLib.TestLibrary/TrashLibIntegrationFixture.cs index 2abfc6a6..6c9e4bec 100644 --- a/src/tests/Recyclarr.TrashLib.TestLibrary/TrashLibIntegrationFixture.cs +++ b/src/tests/Recyclarr.TrashLib.TestLibrary/TrashLibIntegrationFixture.cs @@ -1,96 +1,41 @@ using System.IO.Abstractions; using Autofac; -using Autofac.Features.ResolveAnything; -using Recyclarr.Common; using Recyclarr.TestLibrary; using Recyclarr.TestLibrary.Autofac; using Recyclarr.TrashLib.ApiServices.System; using Recyclarr.TrashLib.Repo.VersionControl; using Recyclarr.TrashLib.Startup; -using Spectre.Console; -using Spectre.Console.Testing; namespace Recyclarr.TrashLib.TestLibrary; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public abstract class TrashLibIntegrationFixture : IDisposable +public abstract class TrashLibIntegrationFixture : IntegrationTestFixture { + protected IAppPaths Paths { get; } + protected TrashLibIntegrationFixture() { - Fs = new MockFileSystem(new MockFileSystemOptions - { - CreateDefaultTempDir = false - }); - Paths = new AppPaths(Fs.CurrentDirectory().SubDirectory("test").SubDirectory("recyclarr")); - - _container = new Lazy(() => - { - var builder = new ContainerBuilder(); - - RegisterTypes(builder); - - builder.RegisterInstance(Fs).As(); - builder.RegisterInstance(Paths).As(); - builder.RegisterInstance(Console).As(); - builder.RegisterInstance(Logger).As(); - - builder.RegisterMockFor(); - builder.RegisterMockFor(); - builder.RegisterMockFor(); - builder.RegisterMockFor(m => - { - // By default, choose some extremely high number so that all the newest features are enabled. - m.GetVersion(default!).ReturnsForAnyArgs(_ => new Version("99.0.0.0")); - }); - - builder.RegisterSource(); - - return builder.Build(); - }); } - // ReSharper disable once VirtualMemberNeverOverridden.Global - // ReSharper disable once UnusedParameter.Global - protected virtual void RegisterTypes(ContainerBuilder builder) + protected override void RegisterTypes(ContainerBuilder builder) { + base.RegisterTypes(builder); builder.RegisterModule(); } - // ReSharper disable MemberCanBePrivate.Global - - private readonly Lazy _container; - protected ILifetimeScope Container => _container.Value; - - protected MockFileSystem Fs { get; } - protected TestConsole Console { get; } = new(); - protected IAppPaths Paths { get; } - protected TestableLogger Logger { get; } = new(); - - // ReSharper restore MemberCanBePrivate.Global - - protected T Resolve() where T : notnull + protected override void RegisterStubsAndMocks(ContainerBuilder builder) { - return Container.Resolve(); - } + base.RegisterStubsAndMocks(builder); - // ReSharper disable once VirtualMemberNeverOverridden.Global - protected virtual void Dispose(bool disposing) - { - if (!disposing) - { - return; - } + builder.RegisterInstance(Paths).As(); - if (_container.IsValueCreated) + builder.RegisterMockFor(); + builder.RegisterMockFor(); + builder.RegisterMockFor(m => { - _container.Value.Dispose(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + // By default, choose some extremely high number so that all the newest features are enabled. + m.GetVersion(default!).ReturnsForAnyArgs(_ => new Version("99.0.0.0")); + }); } }