diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj index 4fdad984..62882e30 100644 --- a/src/Common/Common.csproj +++ b/src/Common/Common.csproj @@ -1,5 +1,6 @@ + diff --git a/src/Common/Extensions/FluentValidationExtensions.cs b/src/Common/Extensions/FluentValidationExtensions.cs new file mode 100644 index 00000000..75af48a6 --- /dev/null +++ b/src/Common/Extensions/FluentValidationExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Validators; + +namespace Common.Extensions +{ + public static class FluentValidationExtensions + { + // From: https://github.com/FluentValidation/FluentValidation/issues/1648 + public static IRuleBuilderOptions SetNonNullableValidator( + this IRuleBuilder ruleBuilder, IValidator validator, params string[] ruleSets) + { + var adapter = new NullableChildValidatorAdaptor(validator, validator.GetType()) + { + RuleSets = ruleSets + }; + + return ruleBuilder.SetAsyncValidator(adapter); + } + + private class NullableChildValidatorAdaptor : ChildValidatorAdaptor, + IPropertyValidator, IAsyncPropertyValidator + { + public NullableChildValidatorAdaptor(IValidator validator, Type validatorType) + : base(validator, validatorType) + { + } + + public override Task IsValidAsync(ValidationContext context, TProperty? value, + CancellationToken cancellation) + { + return base.IsValidAsync(context, value!, cancellation); + } + + public override bool IsValid(ValidationContext context, TProperty? value) + { + return base.IsValid(context, value!); + } + } + } +} diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index be04764d..d3e7bec3 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -2,34 +2,35 @@ - + - - + + + - - + + - + - + diff --git a/src/Trash.TestLibrary/Trash.TestLibrary.csproj b/src/Trash.TestLibrary/Trash.TestLibrary.csproj deleted file mode 100644 index 18753431..00000000 --- a/src/Trash.TestLibrary/Trash.TestLibrary.csproj +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/Trash.Tests/CreateConfig/CreateConfigCommandTest.cs b/src/Trash.Tests/Command/CreateConfigCommandTest.cs similarity index 97% rename from src/Trash.Tests/CreateConfig/CreateConfigCommandTest.cs rename to src/Trash.Tests/Command/CreateConfigCommandTest.cs index ee4124df..01e65823 100644 --- a/src/Trash.Tests/CreateConfig/CreateConfigCommandTest.cs +++ b/src/Trash.Tests/Command/CreateConfigCommandTest.cs @@ -8,7 +8,7 @@ using Trash.Command; // ReSharper disable MethodHasAsyncOverload -namespace Trash.Tests.CreateConfig +namespace Trash.Tests.Command { [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/src/Trash.Tests/Command/CliTypeActivatorTest.cs b/src/Trash.Tests/Command/Helpers/CliTypeActivatorTest.cs similarity index 97% rename from src/Trash.Tests/Command/CliTypeActivatorTest.cs rename to src/Trash.Tests/Command/Helpers/CliTypeActivatorTest.cs index 94ad046f..ebfaddbe 100644 --- a/src/Trash.Tests/Command/CliTypeActivatorTest.cs +++ b/src/Trash.Tests/Command/Helpers/CliTypeActivatorTest.cs @@ -5,7 +5,7 @@ using FluentAssertions; using NUnit.Framework; using Trash.Command.Helpers; -namespace Trash.Tests.Command +namespace Trash.Tests.Command.Helpers { [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/src/Trash.Tests/Config/ConfigurationLoaderTest.cs b/src/Trash.Tests/Config/ConfigurationLoaderTest.cs index 0923ff5a..035aeb01 100644 --- a/src/Trash.Tests/Config/ConfigurationLoaderTest.cs +++ b/src/Trash.Tests/Config/ConfigurationLoaderTest.cs @@ -8,13 +8,14 @@ using System.Text; using Common; using Common.Extensions; using FluentAssertions; -using JetBrains.Annotations; +using FluentValidation; +using FluentValidation.Results; using NSubstitute; using NUnit.Framework; using TestLibrary; using Trash.Config; using TrashLib.Config; -using TrashLib.Sonarr; +using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; using YamlDotNet.Serialization.ObjectFactories; @@ -32,43 +33,10 @@ namespace Trash.Tests.Config [SuppressMessage("Microsoft.Design", "CA1034", Justification = "YamlDotNet requires this type to be public so it may access it")] - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class TestConfigValidFalse : IServiceConfiguration + public class TestConfig : IServiceConfiguration { - public const string Msg = "validate_false"; - public string BaseUrl { get; init; } = ""; - public string ApiKey { get; init; } = ""; - - public bool IsValid(out string msg) - { - msg = Msg; - return false; - } - - public string BuildUrl() - { - throw new NotImplementedException(); - } - } - - [SuppressMessage("Microsoft.Design", "CA1034", - Justification = "YamlDotNet requires this type to be public so it may access it")] - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class TestConfigValidTrue : IServiceConfiguration - { - public string BaseUrl { get; init; } = ""; - public string ApiKey { get; init; } = ""; - - public bool IsValid(out string msg) - { - msg = ""; - return true; - } - - public string BuildUrl() - { - throw new NotImplementedException(); - } + public string BaseUrl => ""; + public string ApiKey => ""; } [Test] @@ -94,7 +62,9 @@ namespace Trash.Tests.Config var actualActiveConfigs = new List(); provider.ActiveConfiguration = Arg.Do(a => actualActiveConfigs.Add(a)); - var loader = new ConfigurationLoader(provider, fs, new DefaultObjectFactory()); + var validator = Substitute.For>(); + var loader = + new ConfigurationLoader(provider, fs, new DefaultObjectFactory(), validator); var fakeFiles = new List { @@ -118,10 +88,12 @@ namespace Trash.Tests.Config [Test] public void Parse_using_stream() { + var validator = Substitute.For>(); var configLoader = new ConfigurationLoader( Substitute.For(), Substitute.For(), - new DefaultObjectFactory()); + new DefaultObjectFactory(), + validator); var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr"); @@ -156,12 +128,20 @@ namespace Trash.Tests.Config } [Test] - public void Validation_failure_throws() + public void Throw_when_validation_fails() { - var configLoader = new ConfigurationLoader( + var validator = Substitute.For>(); + var configLoader = new ConfigurationLoader( Substitute.For(), Substitute.For(), - new DefaultObjectFactory()); + new DefaultObjectFactory(), + validator); + + // force the validator to return a validation error + validator.Validate(Arg.Any()).Returns(new ValidationResult + { + Errors = {new ValidationFailure("PropertyName", "Test Validation Failure")} + }); var testYml = @" fubar: @@ -169,17 +149,18 @@ fubar: "; Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); - act.Should().Throw() - .WithMessage($"*{TestConfigValidFalse.Msg}"); + act.Should().Throw(); } [Test] public void Validation_success_does_not_throw() { - var configLoader = new ConfigurationLoader( + var validator = Substitute.For>(); + var configLoader = new ConfigurationLoader( Substitute.For(), Substitute.For(), - new DefaultObjectFactory()); + new DefaultObjectFactory(), + validator); var testYml = @" fubar: diff --git a/src/Trash.Tests/Config/ServiceConfigurationTest.cs b/src/Trash.Tests/Config/ServiceConfigurationTest.cs deleted file mode 100644 index 9a2b1107..00000000 --- a/src/Trash.Tests/Config/ServiceConfigurationTest.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.IO; -using System.IO.Abstractions; -using FluentAssertions; -using JetBrains.Annotations; -using NSubstitute; -using NUnit.Framework; -using Trash.Config; -using TrashLib.Config; -using YamlDotNet.Core; -using YamlDotNet.Serialization.ObjectFactories; - -namespace Trash.Tests.Config -{ - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ServiceConfigurationTest - { - // This test class must be public otherwise it cannot be deserialized by YamlDotNet - [UsedImplicitly] - private class TestServiceConfiguration : ServiceConfiguration - { - public const string ServiceName = "test_service"; - - public override bool IsValid(out string msg) - { - throw new NotImplementedException(); - } - } - - [Test] - public void Deserialize_ApiKeyMissing_Throw() - { - const string yaml = @" -test_service: -- base_url: a -"; - var loader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), - new DefaultObjectFactory()); - - Action act = () => loader.LoadFromStream(new StringReader(yaml), TestServiceConfiguration.ServiceName); - - act.Should().Throw() - .WithMessage("*Property 'api_key' is required"); - } - - [Test] - public void Deserialize_BaseUrlMissing_Throw() - { - const string yaml = @" -test_service: -- api_key: b -"; - var loader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), - new DefaultObjectFactory()); - - Action act = () => loader.LoadFromStream(new StringReader(yaml), TestServiceConfiguration.ServiceName); - - act.Should().Throw() - .WithMessage("*Property 'base_url' is required"); - } - } -} diff --git a/src/Trash.Tests/Trash.Tests.csproj b/src/Trash.Tests/Trash.Tests.csproj index 9f984b62..1492c910 100644 --- a/src/Trash.Tests/Trash.Tests.csproj +++ b/src/Trash.Tests/Trash.Tests.csproj @@ -5,7 +5,10 @@ - + + + + diff --git a/src/Trash/Command/RadarrCommand.cs b/src/Trash/Command/RadarrCommand.cs index d3c2e70a..3664c095 100644 --- a/src/Trash/Command/RadarrCommand.cs +++ b/src/Trash/Command/RadarrCommand.cs @@ -8,7 +8,7 @@ using Serilog; using Serilog.Core; using Trash.Command.Helpers; using Trash.Config; -using TrashLib.Radarr; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat; using TrashLib.Radarr.QualityDefinition; diff --git a/src/Trash/Command/SonarrCommand.cs b/src/Trash/Command/SonarrCommand.cs index bf05f1f8..496a7d60 100644 --- a/src/Trash/Command/SonarrCommand.cs +++ b/src/Trash/Command/SonarrCommand.cs @@ -8,7 +8,7 @@ using Serilog; using Serilog.Core; using Trash.Command.Helpers; using Trash.Config; -using TrashLib.Sonarr; +using TrashLib.Sonarr.Config; using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.ReleaseProfile; diff --git a/src/Trash/CompositionRoot.cs b/src/Trash/CompositionRoot.cs index e5c315ac..41e678a7 100644 --- a/src/Trash/CompositionRoot.cs +++ b/src/Trash/CompositionRoot.cs @@ -3,6 +3,7 @@ using System.IO; using System.IO.Abstractions; using System.Reflection; using Autofac; +using Autofac.Core.Activators.Reflection; using CliFx; using Serilog; using Serilog.Core; @@ -47,6 +48,7 @@ namespace Trash .As(); builder.RegisterGeneric(typeof(ConfigurationLoader<>)) + .WithProperty(new AutowiringParameter()) .As(typeof(IConfigurationLoader<>)); // note: Do not allow consumers to resolve IServiceConfiguration directly; if this gets cached diff --git a/src/Trash/Config/ConfigurationException.cs b/src/Trash/Config/ConfigurationException.cs index 542e6041..aee050eb 100644 --- a/src/Trash/Config/ConfigurationException.cs +++ b/src/Trash/Config/ConfigurationException.cs @@ -1,18 +1,49 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentValidation.Results; namespace Trash.Config { public class ConfigurationException : Exception { - public ConfigurationException(string propertyName, Type deserializableType, string msg) - : base($"An exception occurred while deserializing type '{deserializableType}' " + - $"for YML property '{propertyName}': {msg}") + private ConfigurationException(string propertyName, Type deserializableType, IEnumerable messages) { PropertyName = propertyName; DeserializableType = deserializableType; + ErrorMessages = messages.ToList(); } + public ConfigurationException(string propertyName, Type deserializableType, string message) + : this(propertyName, deserializableType, new[] {message}) + { + } + + public ConfigurationException(string propertyName, Type deserializableType, + IEnumerable validationFailures) + : this(propertyName, deserializableType, validationFailures.Select(e => e.ToString())) + { + } + + public IReadOnlyCollection ErrorMessages { get; } public string PropertyName { get; } public Type DeserializableType { get; } + + public override string Message => BuildMessage(); + + private string BuildMessage() + { + const string delim = "\n - "; + var builder = new StringBuilder( + $"An exception occurred while deserializing type '{DeserializableType}' for YML property '{PropertyName}'"); + if (ErrorMessages.Count > 0) + { + builder.Append(":" + delim); + builder.Append(string.Join(delim, ErrorMessages)); + } + + return builder.ToString(); + } } } diff --git a/src/Trash/Config/ConfigurationLoader.cs b/src/Trash/Config/ConfigurationLoader.cs index c2f7f338..2e64325e 100644 --- a/src/Trash/Config/ConfigurationLoader.cs +++ b/src/Trash/Config/ConfigurationLoader.cs @@ -3,6 +3,7 @@ using System.IO; using System.IO.Abstractions; using System.Linq; using Common.YamlDotNet; +using FluentValidation; using TrashLib.Config; using YamlDotNet.Core; using YamlDotNet.Core.Events; @@ -17,14 +18,19 @@ namespace Trash.Config private readonly IConfigurationProvider _configProvider; private readonly IDeserializer _deserializer; private readonly IFileSystem _fileSystem; + private readonly IValidator _validator; - public ConfigurationLoader(IConfigurationProvider configProvider, IFileSystem fileSystem, - IObjectFactory objectFactory) + public ConfigurationLoader( + IConfigurationProvider configProvider, + IFileSystem fileSystem, + IObjectFactory objectFactory, + IValidator validator) { _configProvider = configProvider; _fileSystem = fileSystem; + _validator = validator; _deserializer = new DeserializerBuilder() - .WithRequiredPropertyValidation() + .IgnoreUnmatchedProperties() .WithNamingConvention(UnderscoredNamingConvention.Instance) .WithTypeConverter(new YamlNullableEnumTypeConverter()) .WithObjectFactory(objectFactory) @@ -54,9 +60,10 @@ namespace Trash.Config { foreach (var config in configs) { - if (!config.IsValid(out var msg)) + var result = _validator.Validate(config); + if (result is {IsValid: false}) { - throw new ConfigurationException(configSection, typeof(T), msg); + throw new ConfigurationException(configSection, typeof(T), result.Errors); } validConfigs.Add(config); diff --git a/src/Trash.TestLibrary/CfTestUtils.cs b/src/TrashLib.TestLibrary/CfTestUtils.cs similarity index 100% rename from src/Trash.TestLibrary/CfTestUtils.cs rename to src/TrashLib.TestLibrary/CfTestUtils.cs diff --git a/src/TrashLib.TestLibrary/TrashLib.TestLibrary.csproj b/src/TrashLib.TestLibrary/TrashLib.TestLibrary.csproj new file mode 100644 index 00000000..7a394e35 --- /dev/null +++ b/src/TrashLib.TestLibrary/TrashLib.TestLibrary.csproj @@ -0,0 +1,8 @@ + + + Trash.TestLibrary + + + + + diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs index 7be17746..3d5298ba 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs @@ -8,7 +8,7 @@ using NUnit.Framework; using Serilog; using TestLibrary.FluentAssertions; using Trash.TestLibrary; -using TrashLib.Radarr; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Guide; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Processors; diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs index 71763f96..fe844218 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using FluentAssertions; using Newtonsoft.Json.Linq; using NUnit.Framework; -using TrashLib.Radarr; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs index 6c7a4eaf..bd5e2c3a 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; using TestLibrary.FluentAssertions; -using TrashLib.Radarr; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs index 9d0baa8b..a927348c 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs @@ -3,7 +3,7 @@ using FluentAssertions; using Newtonsoft.Json.Linq; using NUnit.Framework; using Trash.TestLibrary; -using TrashLib.Radarr; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs index 919fb1fb..4db56160 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json.Linq; using NSubstitute; using NUnit.Framework; using TrashLib.Config; -using TrashLib.Radarr; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; diff --git a/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs b/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs index cef53d12..5352e932 100644 --- a/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs +++ b/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections; -using System.IO; -using System.IO.Abstractions; +using System.Collections.Generic; +using System.Linq; +using Autofac; using FluentAssertions; -using NSubstitute; +using FluentValidation; using NUnit.Framework; -using Trash.Config; using TrashLib.Config; using TrashLib.Radarr; -using YamlDotNet.Core; -using YamlDotNet.Serialization.ObjectFactories; +using TrashLib.Radarr.Config; +using TrashLib.Radarr.QualityDefinition; namespace TrashLib.Tests.Radarr { @@ -17,105 +15,96 @@ namespace TrashLib.Tests.Radarr [Parallelizable(ParallelScope.All)] public class RadarrConfigurationTest { - public static IEnumerable GetTrashIdsOrNamesEmptyTestData() - { - yield return new TestCaseData(@" -radarr: - - api_key: abc - base_url: xyz - custom_formats: - - names: [foo] - quality_profiles: - - name: MyProfile -") - .SetName("{m} (without_trash_ids)"); + private IContainer _container = default!; - yield return new TestCaseData(@" -radarr: - - api_key: abc - base_url: xyz - custom_formats: - - trash_ids: [abc123] - quality_profiles: - - name: MyProfile -") - .SetName("{m} (without_names)"); + [OneTimeSetUp] + public void Setup() + { + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterModule(); + _container = builder.Build(); } - [TestCaseSource(nameof(GetTrashIdsOrNamesEmptyTestData))] - public void Custom_format_either_names_or_trash_id_not_empty_is_ok(string testYaml) + private static readonly TestCaseData[] NameOrIdsTestData = { - var configLoader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), new DefaultObjectFactory()); - - Action act = () => configLoader.LoadFromStream(new StringReader(testYaml), "radarr"); - - act.Should().NotThrow(); - } + new(new List {"name"}, new List()), + new(new List(), new List {"trash_id"}) + }; - [Test] - public void Custom_format_names_and_trash_ids_lists_must_not_both_be_empty() + [TestCaseSource(nameof(NameOrIdsTestData))] + public void Custom_format_is_valid_with_one_of_either_names_or_trash_id(List namesList, + List trashIdsList) { - var testYaml = @" -radarr: - - api_key: abc - base_url: xyz - custom_formats: - - quality_profiles: - - name: MyProfile -"; - var configLoader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), new DefaultObjectFactory()); + var config = new RadarrConfiguration + { + ApiKey = "required value", + BaseUrl = "required value", + CustomFormats = new List + { + new() {Names = namesList, TrashIds = trashIdsList} + } + }; - Action act = () => configLoader.LoadFromStream(new StringReader(testYaml), "radarr"); + var validator = _container.Resolve>(); + var result = validator.Validate(config); - act.Should().Throw() - .WithMessage("*must contain at least one element in either 'names' or 'trash_ids'."); + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); } [Test] - public void Quality_definition_type_is_required() + public void Validation_fails_for_all_missing_required_properties() { - const string yaml = @" -radarr: -- base_url: a - api_key: b - quality_definition: - preferred_ratio: 0.5 -"; - var loader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), - new DefaultObjectFactory()); + // default construct which should yield default values (invalid) for all required properties + var config = new RadarrConfiguration(); + var validator = _container.Resolve>(); - Action act = () => loader.LoadFromStream(new StringReader(yaml), "radarr"); + var result = validator.Validate(config); - act.Should().Throw() - .WithMessage("*'type' is required for 'quality_definition'"); + var expectedErrorMessageSubstrings = new[] + { + "Property 'base_url' is required", + "Property 'api_key' is required", + "'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'", + "'name' is required for elements under 'quality_profiles'", + "'type' is required for 'quality_definition'" + }; + + result.IsValid.Should().BeFalse(); + result.Errors.Select(e => e.ErrorMessage).Should() + .OnlyContain(x => expectedErrorMessageSubstrings.Any(x.Contains)); } [Test] - public void Quality_profile_name_is_required() + public void Validation_succeeds_when_no_missing_required_properties() { - const string testYaml = @" -radarr: - - api_key: abc - base_url: xyz - custom_formats: - - names: [one, two] - quality_profiles: - - score: 100 -"; - - var configLoader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), new DefaultObjectFactory()); + var config = new RadarrConfiguration + { + ApiKey = "required value", + BaseUrl = "required value", + CustomFormats = new List + { + new() + { + Names = new List{"required value"}, + QualityProfiles = new List + { + new() {Name = "required value"} + } + } + }, + QualityDefinition = new QualityDefinitionConfig + { + Type = RadarrQualityDefinitionType.Movie + } + }; - Action act = () => configLoader.LoadFromStream(new StringReader(testYaml), "radarr"); + var validator = _container.Resolve>(); + var result = validator.Validate(config); - act.Should().Throw(); + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); } } } diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs index efd58bb1..7e584b57 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; -using TrashLib.Sonarr; +using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; namespace TrashLib.Tests.Sonarr.ReleaseProfile diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs index 4a27ef0b..a2a70d88 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs @@ -6,7 +6,7 @@ using NUnit.Framework; using Serilog; using Serilog.Sinks.TestCorrelator; using TestLibrary; -using TrashLib.Sonarr; +using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; namespace TrashLib.Tests.Sonarr.ReleaseProfile diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs index c4ed4126..db4e7384 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs @@ -1,8 +1,8 @@ using NSubstitute; using NUnit.Framework; using Serilog; -using TrashLib.Sonarr; using TrashLib.Sonarr.Api; +using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; namespace TrashLib.Tests.Sonarr diff --git a/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs b/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs index c4a84ecf..42de601f 100644 --- a/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs +++ b/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs @@ -1,14 +1,13 @@ -using System; -using System.IO; -using System.IO.Abstractions; +using System.Collections.Generic; +using System.Linq; +using Autofac; using FluentAssertions; -using NSubstitute; +using FluentValidation; using NUnit.Framework; -using Trash.Config; using TrashLib.Config; using TrashLib.Sonarr; -using YamlDotNet.Core; -using YamlDotNet.Serialization.ObjectFactories; +using TrashLib.Sonarr.Config; +using TrashLib.Sonarr.ReleaseProfile; namespace TrashLib.Tests.Sonarr { @@ -16,25 +15,56 @@ namespace TrashLib.Tests.Sonarr [Parallelizable(ParallelScope.All)] public class SonarrConfigurationTest { + private IContainer _container = default!; + + [OneTimeSetUp] + public void Setup() + { + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterModule(); + _container = builder.Build(); + } + [Test] - public void Deserialize_ReleaseProfileTypeMissing_Throw() + public void Validation_fails_for_all_missing_required_properties() { - const string yaml = @" -sonarr: -- base_url: a - api_key: b - release_profiles: - - strict_negative_scores: true -"; - var loader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), - new DefaultObjectFactory()); - - Action act = () => loader.LoadFromStream(new StringReader(yaml), "sonarr"); - - act.Should().Throw() - .WithMessage("*'type' is required for 'release_profiles' elements"); + // default construct which should yield default values (invalid) for all required properties + var config = new SonarrConfiguration(); + var validator = _container.Resolve>(); + + var result = validator.Validate(config); + + var expectedErrorMessageSubstrings = new[] + { + "Property 'base_url' is required", + "Property 'api_key' is required", + "'type' is required for 'release_profiles' elements" + }; + + result.IsValid.Should().BeFalse(); + result.Errors.Select(e => e.ErrorMessage).Should() + .OnlyContain(x => expectedErrorMessageSubstrings.Any(x.Contains)); + } + + [Test] + public void Validation_succeeds_when_no_missing_required_properties() + { + var config = new SonarrConfiguration + { + ApiKey = "required value", + BaseUrl = "required value", + ReleaseProfiles = new List + { + new() {Type = ReleaseProfileType.Anime} + } + }; + + var validator = _container.Resolve>(); + var result = validator.Validate(config); + + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); } } } diff --git a/src/TrashLib.Tests/TrashLib.Tests.csproj b/src/TrashLib.Tests/TrashLib.Tests.csproj index 9f984b62..62202ad4 100644 --- a/src/TrashLib.Tests/TrashLib.Tests.csproj +++ b/src/TrashLib.Tests/TrashLib.Tests.csproj @@ -5,7 +5,7 @@ - - + + diff --git a/src/TrashLib/Config/ConfigAutofacModule.cs b/src/TrashLib/Config/ConfigAutofacModule.cs index deadcaf4..5640d410 100644 --- a/src/TrashLib/Config/ConfigAutofacModule.cs +++ b/src/TrashLib/Config/ConfigAutofacModule.cs @@ -1,4 +1,7 @@ +using System.Reflection; using Autofac; +using FluentValidation; +using Module = Autofac.Module; namespace TrashLib.Config { @@ -9,6 +12,10 @@ namespace TrashLib.Config builder.RegisterType() .As() .SingleInstance(); + + builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) + .AsClosedTypesOf(typeof(IValidator<>)) + .AsImplementedInterfaces(); } } } diff --git a/src/TrashLib/IServerInfo.cs b/src/TrashLib/Config/IServerInfo.cs similarity index 74% rename from src/TrashLib/IServerInfo.cs rename to src/TrashLib/Config/IServerInfo.cs index ee77e831..c76b6b0d 100644 --- a/src/TrashLib/IServerInfo.cs +++ b/src/TrashLib/Config/IServerInfo.cs @@ -1,4 +1,4 @@ -namespace TrashLib +namespace TrashLib.Config { public interface IServerInfo { diff --git a/src/TrashLib/Config/IServiceConfiguration.cs b/src/TrashLib/Config/IServiceConfiguration.cs index b6599e00..6ffd131f 100644 --- a/src/TrashLib/Config/IServiceConfiguration.cs +++ b/src/TrashLib/Config/IServiceConfiguration.cs @@ -2,8 +2,7 @@ namespace TrashLib.Config { public interface IServiceConfiguration { - string BaseUrl { get; init; } - string ApiKey { get; init; } - bool IsValid(out string msg); + string BaseUrl { get; } + string ApiKey { get; } } } diff --git a/src/TrashLib/ServerInfo.cs b/src/TrashLib/Config/ServerInfo.cs similarity index 94% rename from src/TrashLib/ServerInfo.cs rename to src/TrashLib/Config/ServerInfo.cs index 8fab512e..f4978ec7 100644 --- a/src/TrashLib/ServerInfo.cs +++ b/src/TrashLib/Config/ServerInfo.cs @@ -1,6 +1,6 @@ using Flurl; -namespace TrashLib +namespace TrashLib.Config { internal class ServerInfo : IServerInfo { diff --git a/src/TrashLib/Config/ServiceConfiguration.cs b/src/TrashLib/Config/ServiceConfiguration.cs index 881fcd9b..964d19cf 100644 --- a/src/TrashLib/Config/ServiceConfiguration.cs +++ b/src/TrashLib/Config/ServiceConfiguration.cs @@ -1,15 +1,8 @@ -using System.ComponentModel.DataAnnotations; - -namespace TrashLib.Config +namespace TrashLib.Config { public abstract class ServiceConfiguration : IServiceConfiguration { - [Required(ErrorMessage = "Property 'base_url' is required")] public string BaseUrl { get; init; } = ""; - - [Required(ErrorMessage = "Property 'api_key' is required")] public string ApiKey { get; init; } = ""; - - public abstract bool IsValid(out string msg); } } diff --git a/src/TrashLib/Radarr/Config/IRadarrValidationMessages.cs b/src/TrashLib/Radarr/Config/IRadarrValidationMessages.cs new file mode 100644 index 00000000..b200295d --- /dev/null +++ b/src/TrashLib/Radarr/Config/IRadarrValidationMessages.cs @@ -0,0 +1,11 @@ +namespace TrashLib.Radarr.Config +{ + public interface IRadarrValidationMessages + { + string BaseUrl { get; } + string ApiKey { get; } + string CustomFormatNamesAndIds { get; } + string QualityProfileName { get; } + string QualityDefinitionType { get; } + } +} diff --git a/src/TrashLib/Radarr/RadarrConfiguration.cs b/src/TrashLib/Radarr/Config/RadarrConfiguration.cs similarity index 60% rename from src/TrashLib/Radarr/RadarrConfiguration.cs rename to src/TrashLib/Radarr/Config/RadarrConfiguration.cs index 0cb4cdc4..988ec9bd 100644 --- a/src/TrashLib/Radarr/RadarrConfiguration.cs +++ b/src/TrashLib/Radarr/Config/RadarrConfiguration.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; using JetBrains.Annotations; using TrashLib.Config; using TrashLib.Radarr.QualityDefinition; -namespace TrashLib.Radarr +namespace TrashLib.Radarr.Config { [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class RadarrConfiguration : ServiceConfiguration @@ -13,18 +11,6 @@ namespace TrashLib.Radarr public QualityDefinitionConfig? QualityDefinition { get; init; } public List CustomFormats { get; init; } = new(); public bool DeleteOldCustomFormats { get; init; } - - public override bool IsValid(out string msg) - { - if (CustomFormats.Any(cf => cf.TrashIds.Count + cf.Names.Count == 0)) - { - msg = "'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'."; - return false; - } - - msg = ""; - return true; - } } [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] @@ -38,9 +24,7 @@ namespace TrashLib.Radarr [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class QualityProfileConfig { - [Required(ErrorMessage = "'name' is required for elements under 'quality_profiles'")] public string Name { get; init; } = ""; - public int? Score { get; init; } public bool ResetUnmatchedScores { get; init; } } @@ -48,11 +32,8 @@ namespace TrashLib.Radarr [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class QualityDefinitionConfig { - // -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML - // all of this craziness is to avoid making the enum type nullable which will make using the property - // frustrating. - [EnumDataType(typeof(RadarrQualityDefinitionType), - ErrorMessage = "'type' is required for 'quality_definition'")] + // -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML. + // All of this craziness is to avoid making the enum type nullable. public RadarrQualityDefinitionType Type { get; init; } = (RadarrQualityDefinitionType) (-1); public decimal PreferredRatio { get; set; } = 1.0m; diff --git a/src/TrashLib/Radarr/Config/RadarrConfigurationValidator.cs b/src/TrashLib/Radarr/Config/RadarrConfigurationValidator.cs new file mode 100644 index 00000000..21dcf3c8 --- /dev/null +++ b/src/TrashLib/Radarr/Config/RadarrConfigurationValidator.cs @@ -0,0 +1,52 @@ +using Common.Extensions; +using FluentValidation; +using JetBrains.Annotations; + +namespace TrashLib.Radarr.Config +{ + [UsedImplicitly] + internal class RadarrConfigurationValidator : AbstractValidator + { + public RadarrConfigurationValidator( + IRadarrValidationMessages messages, + IValidator qualityDefinitionConfigValidator, + IValidator customFormatConfigValidator) + { + RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl); + RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey); + RuleFor(x => x.QualityDefinition).SetNonNullableValidator(qualityDefinitionConfigValidator); + RuleForEach(x => x.CustomFormats).SetValidator(customFormatConfigValidator); + } + } + + [UsedImplicitly] + internal class CustomFormatConfigValidator : AbstractValidator + { + public CustomFormatConfigValidator( + IRadarrValidationMessages messages, + IValidator qualityProfileConfigValidator) + { + RuleFor(x => x.Names).NotEmpty().When(x => x.TrashIds.Count == 0) + .WithMessage(messages.CustomFormatNamesAndIds); + RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileConfigValidator); + } + } + + [UsedImplicitly] + internal class QualityProfileConfigValidator : AbstractValidator + { + public QualityProfileConfigValidator(IRadarrValidationMessages messages) + { + RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName); + } + } + + [UsedImplicitly] + internal class QualityDefinitionConfigValidator : AbstractValidator + { + public QualityDefinitionConfigValidator(IRadarrValidationMessages messages) + { + RuleFor(x => x.Type).IsInEnum().WithMessage(messages.QualityDefinitionType); + } + } +} diff --git a/src/TrashLib/Radarr/Config/RadarrValidationMessages.cs b/src/TrashLib/Radarr/Config/RadarrValidationMessages.cs new file mode 100644 index 00000000..b6c69d14 --- /dev/null +++ b/src/TrashLib/Radarr/Config/RadarrValidationMessages.cs @@ -0,0 +1,20 @@ +namespace TrashLib.Radarr.Config +{ + internal class RadarrValidationMessages : IRadarrValidationMessages + { + public string BaseUrl => + "Property 'base_url' is required"; + + public string ApiKey => + "Property 'api_key' is required"; + + public string CustomFormatNamesAndIds => + "'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'"; + + public string QualityProfileName => + "'name' is required for elements under 'quality_profiles'"; + + public string QualityDefinitionType => + "'type' is required for 'quality_definition'"; + } +} diff --git a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs index 8677514c..c54aab1e 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Flurl; using Flurl.Http; using Newtonsoft.Json.Linq; +using TrashLib.Config; using TrashLib.Radarr.CustomFormat.Models; namespace TrashLib.Radarr.CustomFormat.Api diff --git a/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs b/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs index c5a35803..fa2e9898 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Flurl; using Flurl.Http; using Newtonsoft.Json.Linq; +using TrashLib.Config; namespace TrashLib.Radarr.CustomFormat.Api { diff --git a/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs b/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs index 253dffa8..babc10cb 100644 --- a/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs +++ b/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Common.Extensions; using Serilog; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Processors; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; diff --git a/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs b/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs index 06e935b5..58fd89f0 100644 --- a/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs +++ b/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using TrashLib.Radarr.Config; namespace TrashLib.Radarr.CustomFormat { diff --git a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs index 3e0ac62d..4cc5a06e 100644 --- a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs +++ b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using TrashLib.Radarr.Config; namespace TrashLib.Radarr.CustomFormat.Models { diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs index f3471273..8e0b86fe 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Serilog; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Guide; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs index a67d39c0..e724aa32 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Common.Extensions; using MoreLinq.Extensions; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs index fac591fb..610073e5 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Common.Extensions; using Newtonsoft.Json.Linq; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs index dc4d57d9..774bbd54 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs index c9b07e55..5ccd1af5 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs index 1656c7b2..66a5b2e5 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs index 67714565..aaae4f7d 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using TrashLib.Config; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; diff --git a/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs b/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs index 51b54343..f38c6e8a 100644 --- a/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs +++ b/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Flurl; using Flurl.Http; +using TrashLib.Config; using TrashLib.Radarr.QualityDefinition.Api.Objects; namespace TrashLib.Radarr.QualityDefinition.Api diff --git a/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs b/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs index dd946c50..38942bd6 100644 --- a/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using TrashLib.Radarr.Config; namespace TrashLib.Radarr.QualityDefinition { diff --git a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs index ee91deb0..0014ff2b 100644 --- a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Serilog; +using TrashLib.Radarr.Config; using TrashLib.Radarr.QualityDefinition.Api; using TrashLib.Radarr.QualityDefinition.Api.Objects; diff --git a/src/TrashLib/Radarr/RadarrAutofacModule.cs b/src/TrashLib/Radarr/RadarrAutofacModule.cs index e3da3f6e..026df56f 100644 --- a/src/TrashLib/Radarr/RadarrAutofacModule.cs +++ b/src/TrashLib/Radarr/RadarrAutofacModule.cs @@ -1,6 +1,7 @@ using Autofac; using Autofac.Extras.AggregateService; using TrashLib.Config; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat; using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Guide; @@ -21,6 +22,8 @@ namespace TrashLib.Radarr builder.RegisterType().As(); builder.RegisterType().As(); + // Configuration + builder.RegisterType().As(); builder.Register(c => { var config = c.Resolve().ActiveConfiguration; diff --git a/src/TrashLib/Sonarr/Api/SonarrApi.cs b/src/TrashLib/Sonarr/Api/SonarrApi.cs index 643ab71c..d79d7c2e 100644 --- a/src/TrashLib/Sonarr/Api/SonarrApi.cs +++ b/src/TrashLib/Sonarr/Api/SonarrApi.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Flurl; using Flurl.Http; +using TrashLib.Config; using TrashLib.Sonarr.Api.Objects; namespace TrashLib.Sonarr.Api diff --git a/src/TrashLib/Sonarr/Config/ISonarrValidationMessages.cs b/src/TrashLib/Sonarr/Config/ISonarrValidationMessages.cs new file mode 100644 index 00000000..d5308f53 --- /dev/null +++ b/src/TrashLib/Sonarr/Config/ISonarrValidationMessages.cs @@ -0,0 +1,9 @@ +namespace TrashLib.Sonarr.Config +{ + public interface ISonarrValidationMessages + { + string BaseUrl { get; } + string ApiKey { get; } + string ReleaseProfileType { get; } + } +} diff --git a/src/TrashLib/Sonarr/SonarrConfiguration.cs b/src/TrashLib/Sonarr/Config/SonarrConfiguration.cs similarity index 68% rename from src/TrashLib/Sonarr/SonarrConfiguration.cs rename to src/TrashLib/Sonarr/Config/SonarrConfiguration.cs index d023f91f..c966bb82 100644 --- a/src/TrashLib/Sonarr/SonarrConfiguration.cs +++ b/src/TrashLib/Sonarr/Config/SonarrConfiguration.cs @@ -1,33 +1,21 @@ using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using JetBrains.Annotations; using TrashLib.Config; using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Sonarr +namespace TrashLib.Sonarr.Config { - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class SonarrConfiguration : ServiceConfiguration { public IList ReleaseProfiles { get; set; } = new List(); public SonarrQualityDefinitionType? QualityDefinition { get; init; } - - public override bool IsValid(out string msg) - { - msg = ""; - return true; - } } - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class ReleaseProfileConfig { // -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML // all of this craziness is to avoid making the enum type nullable which will make using the property // frustrating. - [EnumDataType(typeof(ReleaseProfileType), - ErrorMessage = "'type' is required for 'release_profiles' elements")] public ReleaseProfileType Type { get; init; } = (ReleaseProfileType) (-1); public bool StrictNegativeScores { get; init; } @@ -35,7 +23,6 @@ namespace TrashLib.Sonarr public ICollection Tags { get; init; } = new List(); } - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class SonarrProfileFilterConfig { public bool IncludeOptional { get; set; } diff --git a/src/TrashLib/Sonarr/Config/SonarrConfigurationValidator.cs b/src/TrashLib/Sonarr/Config/SonarrConfigurationValidator.cs new file mode 100644 index 00000000..662e408a --- /dev/null +++ b/src/TrashLib/Sonarr/Config/SonarrConfigurationValidator.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using JetBrains.Annotations; + +namespace TrashLib.Sonarr.Config +{ + [UsedImplicitly] + internal class SonarrConfigurationValidator : AbstractValidator + { + public SonarrConfigurationValidator( + ISonarrValidationMessages messages, + IValidator releaseProfileConfigValidator) + { + RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl); + RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey); + RuleForEach(x => x.ReleaseProfiles).SetValidator(releaseProfileConfigValidator); + } + } + + [UsedImplicitly] + internal class ReleaseProfileConfigValidator : AbstractValidator + { + public ReleaseProfileConfigValidator(ISonarrValidationMessages messages) + { + RuleFor(x => x.Type).IsInEnum().WithMessage(messages.ReleaseProfileType); + } + } +} diff --git a/src/TrashLib/Sonarr/Config/SonarrValidationMessages.cs b/src/TrashLib/Sonarr/Config/SonarrValidationMessages.cs new file mode 100644 index 00000000..1fd75e1f --- /dev/null +++ b/src/TrashLib/Sonarr/Config/SonarrValidationMessages.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; + +namespace TrashLib.Sonarr.Config +{ + [UsedImplicitly] + internal class SonarrValidationMessages : ISonarrValidationMessages + { + public string BaseUrl => + "Property 'base_url' is required"; + + public string ApiKey => + "Property 'api_key' is required"; + + public string ReleaseProfileType => + "'type' is required for 'release_profiles' elements"; + } +} diff --git a/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs b/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs index e33f7cef..cf9ed1d4 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using TrashLib.Sonarr.Config; namespace TrashLib.Sonarr.QualityDefinition { diff --git a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs index 4ba1822b..c619267d 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Serilog; using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api.Objects; +using TrashLib.Sonarr.Config; namespace TrashLib.Sonarr.QualityDefinition { diff --git a/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs b/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs index f57c72e9..7738bb61 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using TrashLib.Sonarr.Config; namespace TrashLib.Sonarr.ReleaseProfile { diff --git a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs index e6df1faf..8b806013 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using TrashLib.Sonarr.Config; namespace TrashLib.Sonarr.ReleaseProfile { diff --git a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs index 3e9cd360..01fb05d9 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using TrashLib.Sonarr.Config; namespace TrashLib.Sonarr.ReleaseProfile { diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs index aed506e8..d8cbbe16 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs @@ -7,6 +7,7 @@ using Common.Extensions; using Flurl; using Flurl.Http; using Serilog; +using TrashLib.Sonarr.Config; namespace TrashLib.Sonarr.ReleaseProfile { diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs index b25a6b71..74933520 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs @@ -7,6 +7,7 @@ using Serilog; using TrashLib.ExceptionTypes; using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api.Objects; +using TrashLib.Sonarr.Config; namespace TrashLib.Sonarr.ReleaseProfile { diff --git a/src/TrashLib/Sonarr/SonarrAutofacModule.cs b/src/TrashLib/Sonarr/SonarrAutofacModule.cs index 1a745735..6126058f 100644 --- a/src/TrashLib/Sonarr/SonarrAutofacModule.cs +++ b/src/TrashLib/Sonarr/SonarrAutofacModule.cs @@ -1,5 +1,6 @@ using Autofac; using TrashLib.Sonarr.Api; +using TrashLib.Sonarr.Config; using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.ReleaseProfile; @@ -10,6 +11,7 @@ namespace TrashLib.Sonarr protected override void Load(ContainerBuilder builder) { builder.RegisterType().As(); + builder.RegisterType().As(); // Release Profile Support builder.RegisterType().As(); diff --git a/src/TrashLib/TrashLib.csproj b/src/TrashLib/TrashLib.csproj index eef0e7fe..8730575b 100644 --- a/src/TrashLib/TrashLib.csproj +++ b/src/TrashLib/TrashLib.csproj @@ -1,13 +1,14 @@ - - + + - + + diff --git a/src/TrashUpdater.sln b/src/TrashUpdater.sln index ccdee671..837bb1e1 100644 --- a/src/TrashUpdater.sln +++ b/src/TrashUpdater.sln @@ -19,7 +19,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "Common.Tests\Common.Tests.csproj", "{0720939D-1CA6-43D7-BBED-F8F894C4F562}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Trash.TestLibrary", "Trash.TestLibrary\Trash.TestLibrary.csproj", "{33226068-65E3-4890-8671-59A56BA3F6F0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrashLib.TestLibrary", "TrashLib.TestLibrary\TrashLib.TestLibrary.csproj", "{33226068-65E3-4890-8671-59A56BA3F6F0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrashLib", "TrashLib\TrashLib.csproj", "{4F6ACBA6-9A7D-487C-ACC1-787CCC90A381}" EndProject