refactor: fluent validation for configuration

recyclarr
Robert Dailey 4 years ago
parent 1db23e6be9
commit aecd2dc5dc

@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" />
<PackageReference Include="YamlDotNet" /> <PackageReference Include="YamlDotNet" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -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<T, TProperty?> SetNonNullableValidator<T, TProperty>(
this IRuleBuilder<T, TProperty?> ruleBuilder, IValidator<TProperty> validator, params string[] ruleSets)
{
var adapter = new NullableChildValidatorAdaptor<T, TProperty>(validator, validator.GetType())
{
RuleSets = ruleSets
};
return ruleBuilder.SetAsyncValidator(adapter);
}
private class NullableChildValidatorAdaptor<T, TProperty> : ChildValidatorAdaptor<T, TProperty>,
IPropertyValidator<T, TProperty?>, IAsyncPropertyValidator<T, TProperty?>
{
public NullableChildValidatorAdaptor(IValidator<TProperty> validator, Type validatorType)
: base(validator, validatorType)
{
}
public override Task<bool> IsValidAsync(ValidationContext<T> context, TProperty? value,
CancellationToken cancellation)
{
return base.IsValidAsync(context, value!, cancellation);
}
public override bool IsValid(ValidationContext<T> context, TProperty? value)
{
return base.IsValid(context, value!);
}
}
}
}

@ -2,34 +2,35 @@
<ItemGroup> <ItemGroup>
<!-- Test Packages --> <!-- Test Packages -->
<PackageReference Update="AutofacContrib.NSubstitute" Version="7.*" /> <PackageReference Update="AutofacContrib.NSubstitute" Version="7.*" />
<PackageReference Update="FluentAssertions.Json" Version="5.*" />
<PackageReference Update="FluentAssertions" Version="5.*" /> <PackageReference Update="FluentAssertions" Version="5.*" />
<PackageReference Update="FluentAssertions.Json" Version="5.*" />
<PackageReference Update="GitHubActionsTestLogger" Version="1.*" /> <PackageReference Update="GitHubActionsTestLogger" Version="1.*" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.*" /> <PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.*" />
<PackageReference Update="NSubstitute.Analyzers.CSharp" Version="1.*" />
<PackageReference Update="NSubstitute" Version="4.*" /> <PackageReference Update="NSubstitute" Version="4.*" />
<PackageReference Update="NUnit.Analyzers" Version="3.*" /> <PackageReference Update="NSubstitute.Analyzers.CSharp" Version="1.*" />
<PackageReference Update="NUnit" Version="3.*" /> <PackageReference Update="NUnit" Version="3.*" />
<PackageReference Update="NUnit.Analyzers" Version="3.*" />
<PackageReference Update="NUnit3TestAdapter" Version="3.*" /> <PackageReference Update="NUnit3TestAdapter" Version="3.*" />
<PackageReference Update="Serilog.Sinks.NUnit" Version="1.*" /> <PackageReference Update="Serilog.Sinks.NUnit" Version="1.*" />
<PackageReference Update="Serilog.Sinks.TestCorrelator" Version="3.*" /> <PackageReference Update="Serilog.Sinks.TestCorrelator" Version="3.*" />
<!-- Non-Test Packages --> <!-- Non-Test Packages -->
<PackageReference Update="Autofac" Version="6.*" />
<PackageReference Update="Autofac.Extensions.DependencyInjection" Version="7.*" /> <PackageReference Update="Autofac.Extensions.DependencyInjection" Version="7.*" />
<PackageReference Update="Autofac.Extras.AggregateService" Version="6.*" /> <PackageReference Update="Autofac.Extras.AggregateService" Version="6.*" />
<PackageReference Update="Autofac" Version="6.*" />
<PackageReference Update="CliFx" Version="2.*" /> <PackageReference Update="CliFx" Version="2.*" />
<PackageReference Update="Flurl.Http" Version="3.*" /> <PackageReference Update="FluentValidation" Version="10.*" />
<PackageReference Update="Flurl" Version="3.*" /> <PackageReference Update="Flurl" Version="3.*" />
<PackageReference Update="Flurl.Http" Version="3.*" />
<PackageReference Update="JetBrains.Annotations" Version="*"/> <PackageReference Update="JetBrains.Annotations" Version="*"/>
<PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.*"/> <PackageReference Update="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.*"/>
<PackageReference Update="morelinq" Version="3.*" />
<PackageReference Update="Nerdbank.GitVersioning" Version="3.*"/> <PackageReference Update="Nerdbank.GitVersioning" Version="3.*"/>
<PackageReference Update="Serilog" Version="2.*" />
<PackageReference Update="Serilog.Sinks.Console" Version="3.*" /> <PackageReference Update="Serilog.Sinks.Console" Version="3.*" />
<PackageReference Update="Serilog.Sinks.File" Version="4.*" /> <PackageReference Update="Serilog.Sinks.File" Version="4.*" />
<PackageReference Update="Serilog" Version="2.*" />
<PackageReference Update="System.Data.HashFunction.FNV" Version="2.*" /> <PackageReference Update="System.Data.HashFunction.FNV" Version="2.*" />
<PackageReference Update="System.IO.Abstractions" Version="13.*" /> <PackageReference Update="System.IO.Abstractions" Version="13.*" />
<PackageReference Update="YamlDotNet" Version="10.*" /> <PackageReference Update="YamlDotNet" Version="10.*" />
<PackageReference Update="morelinq" Version="3.*" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -1,5 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Trash\Trash.csproj" />
</ItemGroup>
</Project>

@ -8,7 +8,7 @@ using Trash.Command;
// ReSharper disable MethodHasAsyncOverload // ReSharper disable MethodHasAsyncOverload
namespace Trash.Tests.CreateConfig namespace Trash.Tests.Command
{ {
[TestFixture] [TestFixture]
[Parallelizable(ParallelScope.All)] [Parallelizable(ParallelScope.All)]

@ -5,7 +5,7 @@ using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using Trash.Command.Helpers; using Trash.Command.Helpers;
namespace Trash.Tests.Command namespace Trash.Tests.Command.Helpers
{ {
[TestFixture] [TestFixture]
[Parallelizable(ParallelScope.All)] [Parallelizable(ParallelScope.All)]

@ -8,13 +8,14 @@ using System.Text;
using Common; using Common;
using Common.Extensions; using Common.Extensions;
using FluentAssertions; using FluentAssertions;
using JetBrains.Annotations; using FluentValidation;
using FluentValidation.Results;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using TestLibrary; using TestLibrary;
using Trash.Config; using Trash.Config;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Sonarr; using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile;
using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectFactories;
@ -32,43 +33,10 @@ namespace Trash.Tests.Config
[SuppressMessage("Microsoft.Design", "CA1034", [SuppressMessage("Microsoft.Design", "CA1034",
Justification = "YamlDotNet requires this type to be public so it may access it")] Justification = "YamlDotNet requires this type to be public so it may access it")]
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public class TestConfig : IServiceConfiguration
public class TestConfigValidFalse : IServiceConfiguration
{ {
public const string Msg = "validate_false"; public string BaseUrl => "";
public string BaseUrl { get; init; } = ""; public string ApiKey => "";
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();
}
} }
[Test] [Test]
@ -94,7 +62,9 @@ namespace Trash.Tests.Config
var actualActiveConfigs = new List<SonarrConfiguration>(); var actualActiveConfigs = new List<SonarrConfiguration>();
provider.ActiveConfiguration = Arg.Do<SonarrConfiguration>(a => actualActiveConfigs.Add(a)); provider.ActiveConfiguration = Arg.Do<SonarrConfiguration>(a => actualActiveConfigs.Add(a));
var loader = new ConfigurationLoader<SonarrConfiguration>(provider, fs, new DefaultObjectFactory()); var validator = Substitute.For<IValidator<SonarrConfiguration>>();
var loader =
new ConfigurationLoader<SonarrConfiguration>(provider, fs, new DefaultObjectFactory(), validator);
var fakeFiles = new List<string> var fakeFiles = new List<string>
{ {
@ -118,10 +88,12 @@ namespace Trash.Tests.Config
[Test] [Test]
public void Parse_using_stream() public void Parse_using_stream()
{ {
var validator = Substitute.For<IValidator<SonarrConfiguration>>();
var configLoader = new ConfigurationLoader<SonarrConfiguration>( var configLoader = new ConfigurationLoader<SonarrConfiguration>(
Substitute.For<IConfigurationProvider>(), Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(), Substitute.For<IFileSystem>(),
new DefaultObjectFactory()); new DefaultObjectFactory(),
validator);
var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr"); var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr");
@ -156,12 +128,20 @@ namespace Trash.Tests.Config
} }
[Test] [Test]
public void Validation_failure_throws() public void Throw_when_validation_fails()
{ {
var configLoader = new ConfigurationLoader<TestConfigValidFalse>( var validator = Substitute.For<IValidator<TestConfig>>();
var configLoader = new ConfigurationLoader<TestConfig>(
Substitute.For<IConfigurationProvider>(), Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(), Substitute.For<IFileSystem>(),
new DefaultObjectFactory()); new DefaultObjectFactory(),
validator);
// force the validator to return a validation error
validator.Validate(Arg.Any<TestConfig>()).Returns(new ValidationResult
{
Errors = {new ValidationFailure("PropertyName", "Test Validation Failure")}
});
var testYml = @" var testYml = @"
fubar: fubar:
@ -169,17 +149,18 @@ fubar:
"; ";
Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar");
act.Should().Throw<ConfigurationException>() act.Should().Throw<ConfigurationException>();
.WithMessage($"*{TestConfigValidFalse.Msg}");
} }
[Test] [Test]
public void Validation_success_does_not_throw() public void Validation_success_does_not_throw()
{ {
var configLoader = new ConfigurationLoader<TestConfigValidTrue>( var validator = Substitute.For<IValidator<TestConfig>>();
var configLoader = new ConfigurationLoader<TestConfig>(
Substitute.For<IConfigurationProvider>(), Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(), Substitute.For<IFileSystem>(),
new DefaultObjectFactory()); new DefaultObjectFactory(),
validator);
var testYml = @" var testYml = @"
fubar: fubar:

@ -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<TestServiceConfiguration>(
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(),
new DefaultObjectFactory());
Action act = () => loader.LoadFromStream(new StringReader(yaml), TestServiceConfiguration.ServiceName);
act.Should().Throw<YamlException>()
.WithMessage("*Property 'api_key' is required");
}
[Test]
public void Deserialize_BaseUrlMissing_Throw()
{
const string yaml = @"
test_service:
- api_key: b
";
var loader = new ConfigurationLoader<TestServiceConfiguration>(
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(),
new DefaultObjectFactory());
Action act = () => loader.LoadFromStream(new StringReader(yaml), TestServiceConfiguration.ServiceName);
act.Should().Throw<YamlException>()
.WithMessage("*Property 'base_url' is required");
}
}
}

@ -5,7 +5,10 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\TestLibrary\TestLibrary.csproj" /> <ProjectReference Include="..\TestLibrary\TestLibrary.csproj" />
<ProjectReference Include="..\Trash.TestLibrary\Trash.TestLibrary.csproj" />
<ProjectReference Include="..\Trash\Trash.csproj" /> <ProjectReference Include="..\Trash\Trash.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="CreateConfig" />
</ItemGroup>
</Project> </Project>

@ -8,7 +8,7 @@ using Serilog;
using Serilog.Core; using Serilog.Core;
using Trash.Command.Helpers; using Trash.Command.Helpers;
using Trash.Config; using Trash.Config;
using TrashLib.Radarr; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat; using TrashLib.Radarr.CustomFormat;
using TrashLib.Radarr.QualityDefinition; using TrashLib.Radarr.QualityDefinition;

@ -8,7 +8,7 @@ using Serilog;
using Serilog.Core; using Serilog.Core;
using Trash.Command.Helpers; using Trash.Command.Helpers;
using Trash.Config; using Trash.Config;
using TrashLib.Sonarr; using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.QualityDefinition;
using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile;

@ -3,6 +3,7 @@ using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Reflection; using System.Reflection;
using Autofac; using Autofac;
using Autofac.Core.Activators.Reflection;
using CliFx; using CliFx;
using Serilog; using Serilog;
using Serilog.Core; using Serilog.Core;
@ -47,6 +48,7 @@ namespace Trash
.As<IObjectFactory>(); .As<IObjectFactory>();
builder.RegisterGeneric(typeof(ConfigurationLoader<>)) builder.RegisterGeneric(typeof(ConfigurationLoader<>))
.WithProperty(new AutowiringParameter())
.As(typeof(IConfigurationLoader<>)); .As(typeof(IConfigurationLoader<>));
// note: Do not allow consumers to resolve IServiceConfiguration directly; if this gets cached // note: Do not allow consumers to resolve IServiceConfiguration directly; if this gets cached

@ -1,18 +1,49 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentValidation.Results;
namespace Trash.Config namespace Trash.Config
{ {
public class ConfigurationException : Exception public class ConfigurationException : Exception
{ {
public ConfigurationException(string propertyName, Type deserializableType, string msg) private ConfigurationException(string propertyName, Type deserializableType, IEnumerable<string> messages)
: base($"An exception occurred while deserializing type '{deserializableType}' " +
$"for YML property '{propertyName}': {msg}")
{ {
PropertyName = propertyName; PropertyName = propertyName;
DeserializableType = deserializableType; 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<ValidationFailure> validationFailures)
: this(propertyName, deserializableType, validationFailures.Select(e => e.ToString()))
{
}
public IReadOnlyCollection<string> ErrorMessages { get; }
public string PropertyName { get; } public string PropertyName { get; }
public Type DeserializableType { 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();
}
} }
} }

@ -3,6 +3,7 @@ using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using Common.YamlDotNet; using Common.YamlDotNet;
using FluentValidation;
using TrashLib.Config; using TrashLib.Config;
using YamlDotNet.Core; using YamlDotNet.Core;
using YamlDotNet.Core.Events; using YamlDotNet.Core.Events;
@ -17,14 +18,19 @@ namespace Trash.Config
private readonly IConfigurationProvider _configProvider; private readonly IConfigurationProvider _configProvider;
private readonly IDeserializer _deserializer; private readonly IDeserializer _deserializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IValidator<T> _validator;
public ConfigurationLoader(IConfigurationProvider configProvider, IFileSystem fileSystem, public ConfigurationLoader(
IObjectFactory objectFactory) IConfigurationProvider configProvider,
IFileSystem fileSystem,
IObjectFactory objectFactory,
IValidator<T> validator)
{ {
_configProvider = configProvider; _configProvider = configProvider;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_validator = validator;
_deserializer = new DeserializerBuilder() _deserializer = new DeserializerBuilder()
.WithRequiredPropertyValidation() .IgnoreUnmatchedProperties()
.WithNamingConvention(UnderscoredNamingConvention.Instance) .WithNamingConvention(UnderscoredNamingConvention.Instance)
.WithTypeConverter(new YamlNullableEnumTypeConverter()) .WithTypeConverter(new YamlNullableEnumTypeConverter())
.WithObjectFactory(objectFactory) .WithObjectFactory(objectFactory)
@ -54,9 +60,10 @@ namespace Trash.Config
{ {
foreach (var config in configs) 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); validConfigs.Add(config);

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Trash.TestLibrary</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TrashLib\TrashLib.csproj" />
</ItemGroup>
</Project>

@ -8,7 +8,7 @@ using NUnit.Framework;
using Serilog; using Serilog;
using TestLibrary.FluentAssertions; using TestLibrary.FluentAssertions;
using Trash.TestLibrary; using Trash.TestLibrary;
using TrashLib.Radarr; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Guide; using TrashLib.Radarr.CustomFormat.Guide;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Processors; using TrashLib.Radarr.CustomFormat.Processors;

@ -2,7 +2,7 @@ using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using TrashLib.Radarr; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;
using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps;

@ -5,7 +5,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using TestLibrary.FluentAssertions; using TestLibrary.FluentAssertions;
using TrashLib.Radarr; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;
using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps;

@ -3,7 +3,7 @@ using FluentAssertions;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using Trash.TestLibrary; using Trash.TestLibrary;
using TrashLib.Radarr; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps;

@ -5,7 +5,7 @@ using Newtonsoft.Json.Linq;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Radarr; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;

@ -1,15 +1,13 @@
using System; using System.Collections.Generic;
using System.Collections; using System.Linq;
using System.IO; using Autofac;
using System.IO.Abstractions;
using FluentAssertions; using FluentAssertions;
using NSubstitute; using FluentValidation;
using NUnit.Framework; using NUnit.Framework;
using Trash.Config;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Radarr; using TrashLib.Radarr;
using YamlDotNet.Core; using TrashLib.Radarr.Config;
using YamlDotNet.Serialization.ObjectFactories; using TrashLib.Radarr.QualityDefinition;
namespace TrashLib.Tests.Radarr namespace TrashLib.Tests.Radarr
{ {
@ -17,105 +15,96 @@ namespace TrashLib.Tests.Radarr
[Parallelizable(ParallelScope.All)] [Parallelizable(ParallelScope.All)]
public class RadarrConfigurationTest public class RadarrConfigurationTest
{ {
public static IEnumerable GetTrashIdsOrNamesEmptyTestData() private IContainer _container = default!;
{
yield return new TestCaseData(@"
radarr:
- api_key: abc
base_url: xyz
custom_formats:
- names: [foo]
quality_profiles:
- name: MyProfile
")
.SetName("{m} (without_trash_ids)");
yield return new TestCaseData(@" [OneTimeSetUp]
radarr: public void Setup()
- api_key: abc {
base_url: xyz var builder = new ContainerBuilder();
custom_formats: builder.RegisterModule<ConfigAutofacModule>();
- trash_ids: [abc123] builder.RegisterModule<RadarrAutofacModule>();
quality_profiles: _container = builder.Build();
- name: MyProfile
")
.SetName("{m} (without_names)");
} }
[TestCaseSource(nameof(GetTrashIdsOrNamesEmptyTestData))] private static readonly TestCaseData[] NameOrIdsTestData =
public void Custom_format_either_names_or_trash_id_not_empty_is_ok(string testYaml)
{ {
var configLoader = new ConfigurationLoader<RadarrConfiguration>( new(new List<string> {"name"}, new List<string>()),
Substitute.For<IConfigurationProvider>(), new(new List<string>(), new List<string> {"trash_id"})
Substitute.For<IFileSystem>(), new DefaultObjectFactory()); };
Action act = () => configLoader.LoadFromStream(new StringReader(testYaml), "radarr");
act.Should().NotThrow();
}
[Test] [TestCaseSource(nameof(NameOrIdsTestData))]
public void Custom_format_names_and_trash_ids_lists_must_not_both_be_empty() public void Custom_format_is_valid_with_one_of_either_names_or_trash_id(List<string> namesList,
List<string> trashIdsList)
{ {
var testYaml = @" var config = new RadarrConfiguration
radarr: {
- api_key: abc ApiKey = "required value",
base_url: xyz BaseUrl = "required value",
custom_formats: CustomFormats = new List<CustomFormatConfig>
- quality_profiles: {
- name: MyProfile new() {Names = namesList, TrashIds = trashIdsList}
"; }
var configLoader = new ConfigurationLoader<RadarrConfiguration>( };
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(), new DefaultObjectFactory());
Action act = () => configLoader.LoadFromStream(new StringReader(testYaml), "radarr"); var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
act.Should().Throw<ConfigurationException>() result.IsValid.Should().BeTrue();
.WithMessage("*must contain at least one element in either 'names' or 'trash_ids'."); result.Errors.Should().BeEmpty();
} }
[Test] [Test]
public void Quality_definition_type_is_required() public void Validation_fails_for_all_missing_required_properties()
{ {
const string yaml = @" // default construct which should yield default values (invalid) for all required properties
radarr: var config = new RadarrConfiguration();
- base_url: a var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
api_key: b
quality_definition:
preferred_ratio: 0.5
";
var loader = new ConfigurationLoader<RadarrConfiguration>(
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(),
new DefaultObjectFactory());
Action act = () => loader.LoadFromStream(new StringReader(yaml), "radarr"); var result = validator.Validate(config);
act.Should().Throw<YamlException>() var expectedErrorMessageSubstrings = new[]
.WithMessage("*'type' is required for 'quality_definition'"); {
"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] [Test]
public void Quality_profile_name_is_required() public void Validation_succeeds_when_no_missing_required_properties()
{ {
const string testYaml = @" var config = new RadarrConfiguration
radarr: {
- api_key: abc ApiKey = "required value",
base_url: xyz BaseUrl = "required value",
custom_formats: CustomFormats = new List<CustomFormatConfig>
- names: [one, two] {
quality_profiles: new()
- score: 100 {
"; Names = new List<string>{"required value"},
QualityProfiles = new List<QualityProfileConfig>
var configLoader = new ConfigurationLoader<RadarrConfiguration>( {
Substitute.For<IConfigurationProvider>(), new() {Name = "required value"}
Substitute.For<IFileSystem>(), new DefaultObjectFactory()); }
}
},
QualityDefinition = new QualityDefinitionConfig
{
Type = RadarrQualityDefinitionType.Movie
}
};
Action act = () => configLoader.LoadFromStream(new StringReader(testYaml), "radarr"); var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
act.Should().Throw<YamlException>(); result.IsValid.Should().BeTrue();
result.Errors.Should().BeEmpty();
} }
} }
} }

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using TrashLib.Sonarr; using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile;
namespace TrashLib.Tests.Sonarr.ReleaseProfile namespace TrashLib.Tests.Sonarr.ReleaseProfile

@ -6,7 +6,7 @@ using NUnit.Framework;
using Serilog; using Serilog;
using Serilog.Sinks.TestCorrelator; using Serilog.Sinks.TestCorrelator;
using TestLibrary; using TestLibrary;
using TrashLib.Sonarr; using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile;
namespace TrashLib.Tests.Sonarr.ReleaseProfile namespace TrashLib.Tests.Sonarr.ReleaseProfile

@ -1,8 +1,8 @@
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Serilog; using Serilog;
using TrashLib.Sonarr;
using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api;
using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile;
namespace TrashLib.Tests.Sonarr namespace TrashLib.Tests.Sonarr

@ -1,14 +1,13 @@
using System; using System.Collections.Generic;
using System.IO; using System.Linq;
using System.IO.Abstractions; using Autofac;
using FluentAssertions; using FluentAssertions;
using NSubstitute; using FluentValidation;
using NUnit.Framework; using NUnit.Framework;
using Trash.Config;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Sonarr; using TrashLib.Sonarr;
using YamlDotNet.Core; using TrashLib.Sonarr.Config;
using YamlDotNet.Serialization.ObjectFactories; using TrashLib.Sonarr.ReleaseProfile;
namespace TrashLib.Tests.Sonarr namespace TrashLib.Tests.Sonarr
{ {
@ -16,25 +15,56 @@ namespace TrashLib.Tests.Sonarr
[Parallelizable(ParallelScope.All)] [Parallelizable(ParallelScope.All)]
public class SonarrConfigurationTest public class SonarrConfigurationTest
{ {
private IContainer _container = default!;
[OneTimeSetUp]
public void Setup()
{
var builder = new ContainerBuilder();
builder.RegisterModule<ConfigAutofacModule>();
builder.RegisterModule<SonarrAutofacModule>();
_container = builder.Build();
}
[Test] [Test]
public void Deserialize_ReleaseProfileTypeMissing_Throw() public void Validation_fails_for_all_missing_required_properties()
{ {
const string yaml = @" // default construct which should yield default values (invalid) for all required properties
sonarr: var config = new SonarrConfiguration();
- base_url: a var validator = _container.Resolve<IValidator<SonarrConfiguration>>();
api_key: b
release_profiles: var result = validator.Validate(config);
- strict_negative_scores: true
"; var expectedErrorMessageSubstrings = new[]
var loader = new ConfigurationLoader<SonarrConfiguration>( {
Substitute.For<IConfigurationProvider>(), "Property 'base_url' is required",
Substitute.For<IFileSystem>(), "Property 'api_key' is required",
new DefaultObjectFactory()); "'type' is required for 'release_profiles' elements"
};
Action act = () => loader.LoadFromStream(new StringReader(yaml), "sonarr");
result.IsValid.Should().BeFalse();
act.Should().Throw<YamlException>() result.Errors.Select(e => e.ErrorMessage).Should()
.WithMessage("*'type' is required for 'release_profiles' elements"); .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<ReleaseProfileConfig>
{
new() {Type = ReleaseProfileType.Anime}
}
};
var validator = _container.Resolve<IValidator<SonarrConfiguration>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();
result.Errors.Should().BeEmpty();
} }
} }
} }

@ -5,7 +5,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\TestLibrary\TestLibrary.csproj" /> <ProjectReference Include="..\TestLibrary\TestLibrary.csproj" />
<ProjectReference Include="..\Trash.TestLibrary\Trash.TestLibrary.csproj" /> <ProjectReference Include="..\TrashLib.TestLibrary\TrashLib.TestLibrary.csproj" />
<ProjectReference Include="..\Trash\Trash.csproj" /> <ProjectReference Include="..\TrashLib\TrashLib.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -1,4 +1,7 @@
using System.Reflection;
using Autofac; using Autofac;
using FluentValidation;
using Module = Autofac.Module;
namespace TrashLib.Config namespace TrashLib.Config
{ {
@ -9,6 +12,10 @@ namespace TrashLib.Config
builder.RegisterType<ConfigurationProvider>() builder.RegisterType<ConfigurationProvider>()
.As<IConfigurationProvider>() .As<IConfigurationProvider>()
.SingleInstance(); .SingleInstance();
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AsClosedTypesOf(typeof(IValidator<>))
.AsImplementedInterfaces();
} }
} }
} }

@ -1,4 +1,4 @@
namespace TrashLib namespace TrashLib.Config
{ {
public interface IServerInfo public interface IServerInfo
{ {

@ -2,8 +2,7 @@ namespace TrashLib.Config
{ {
public interface IServiceConfiguration public interface IServiceConfiguration
{ {
string BaseUrl { get; init; } string BaseUrl { get; }
string ApiKey { get; init; } string ApiKey { get; }
bool IsValid(out string msg);
} }
} }

@ -1,6 +1,6 @@
using Flurl; using Flurl;
namespace TrashLib namespace TrashLib.Config
{ {
internal class ServerInfo : IServerInfo internal class ServerInfo : IServerInfo
{ {

@ -1,15 +1,8 @@
using System.ComponentModel.DataAnnotations; namespace TrashLib.Config
namespace TrashLib.Config
{ {
public abstract class ServiceConfiguration : IServiceConfiguration public abstract class ServiceConfiguration : IServiceConfiguration
{ {
[Required(ErrorMessage = "Property 'base_url' is required")]
public string BaseUrl { get; init; } = ""; public string BaseUrl { get; init; } = "";
[Required(ErrorMessage = "Property 'api_key' is required")]
public string ApiKey { get; init; } = ""; public string ApiKey { get; init; } = "";
public abstract bool IsValid(out string msg);
} }
} }

@ -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; }
}
}

@ -1,11 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Radarr.QualityDefinition; using TrashLib.Radarr.QualityDefinition;
namespace TrashLib.Radarr namespace TrashLib.Radarr.Config
{ {
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class RadarrConfiguration : ServiceConfiguration public class RadarrConfiguration : ServiceConfiguration
@ -13,18 +11,6 @@ namespace TrashLib.Radarr
public QualityDefinitionConfig? QualityDefinition { get; init; } public QualityDefinitionConfig? QualityDefinition { get; init; }
public List<CustomFormatConfig> CustomFormats { get; init; } = new(); public List<CustomFormatConfig> CustomFormats { get; init; } = new();
public bool DeleteOldCustomFormats { get; init; } 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)] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
@ -38,9 +24,7 @@ namespace TrashLib.Radarr
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class QualityProfileConfig public class QualityProfileConfig
{ {
[Required(ErrorMessage = "'name' is required for elements under 'quality_profiles'")]
public string Name { get; init; } = ""; public string Name { get; init; } = "";
public int? Score { get; init; } public int? Score { get; init; }
public bool ResetUnmatchedScores { get; init; } public bool ResetUnmatchedScores { get; init; }
} }
@ -48,11 +32,8 @@ namespace TrashLib.Radarr
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class QualityDefinitionConfig 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 // -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 // All of this craziness is to avoid making the enum type nullable.
// frustrating.
[EnumDataType(typeof(RadarrQualityDefinitionType),
ErrorMessage = "'type' is required for 'quality_definition'")]
public RadarrQualityDefinitionType Type { get; init; } = (RadarrQualityDefinitionType) (-1); public RadarrQualityDefinitionType Type { get; init; } = (RadarrQualityDefinitionType) (-1);
public decimal PreferredRatio { get; set; } = 1.0m; public decimal PreferredRatio { get; set; } = 1.0m;

@ -0,0 +1,52 @@
using Common.Extensions;
using FluentValidation;
using JetBrains.Annotations;
namespace TrashLib.Radarr.Config
{
[UsedImplicitly]
internal class RadarrConfigurationValidator : AbstractValidator<RadarrConfiguration>
{
public RadarrConfigurationValidator(
IRadarrValidationMessages messages,
IValidator<QualityDefinitionConfig> qualityDefinitionConfigValidator,
IValidator<CustomFormatConfig> 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<CustomFormatConfig>
{
public CustomFormatConfigValidator(
IRadarrValidationMessages messages,
IValidator<QualityProfileConfig> 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<QualityProfileConfig>
{
public QualityProfileConfigValidator(IRadarrValidationMessages messages)
{
RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName);
}
}
[UsedImplicitly]
internal class QualityDefinitionConfigValidator : AbstractValidator<QualityDefinitionConfig>
{
public QualityDefinitionConfigValidator(IRadarrValidationMessages messages)
{
RuleFor(x => x.Type).IsInEnum().WithMessage(messages.QualityDefinitionType);
}
}
}

@ -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'";
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Flurl; using Flurl;
using Flurl.Http; using Flurl.Http;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using TrashLib.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Api namespace TrashLib.Radarr.CustomFormat.Api

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Flurl; using Flurl;
using Flurl.Http; using Flurl.Http;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using TrashLib.Config;
namespace TrashLib.Radarr.CustomFormat.Api namespace TrashLib.Radarr.CustomFormat.Api
{ {

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Common.Extensions; using Common.Extensions;
using Serilog; using Serilog;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Processors; using TrashLib.Radarr.CustomFormat.Processors;
using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps;

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Radarr.Config;
namespace TrashLib.Radarr.CustomFormat namespace TrashLib.Radarr.CustomFormat
{ {

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using TrashLib.Radarr.Config;
namespace TrashLib.Radarr.CustomFormat.Models namespace TrashLib.Radarr.CustomFormat.Models
{ {

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Serilog; using Serilog;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Guide; using TrashLib.Radarr.CustomFormat.Guide;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Common.Extensions; using Common.Extensions;
using MoreLinq.Extensions; using MoreLinq.Extensions;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Common.Extensions; using Common.Extensions;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;

@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Flurl; using Flurl;
using Flurl.Http; using Flurl.Http;
using TrashLib.Config;
using TrashLib.Radarr.QualityDefinition.Api.Objects; using TrashLib.Radarr.QualityDefinition.Api.Objects;
namespace TrashLib.Radarr.QualityDefinition.Api namespace TrashLib.Radarr.QualityDefinition.Api

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Radarr.Config;
namespace TrashLib.Radarr.QualityDefinition namespace TrashLib.Radarr.QualityDefinition
{ {

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Serilog; using Serilog;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.QualityDefinition.Api; using TrashLib.Radarr.QualityDefinition.Api;
using TrashLib.Radarr.QualityDefinition.Api.Objects; using TrashLib.Radarr.QualityDefinition.Api.Objects;

@ -1,6 +1,7 @@
using Autofac; using Autofac;
using Autofac.Extras.AggregateService; using Autofac.Extras.AggregateService;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat; using TrashLib.Radarr.CustomFormat;
using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Guide; using TrashLib.Radarr.CustomFormat.Guide;
@ -21,6 +22,8 @@ namespace TrashLib.Radarr
builder.RegisterType<CustomFormatService>().As<ICustomFormatService>(); builder.RegisterType<CustomFormatService>().As<ICustomFormatService>();
builder.RegisterType<QualityProfileService>().As<IQualityProfileService>(); builder.RegisterType<QualityProfileService>().As<IQualityProfileService>();
// Configuration
builder.RegisterType<RadarrValidationMessages>().As<IRadarrValidationMessages>();
builder.Register(c => builder.Register(c =>
{ {
var config = c.Resolve<IConfigurationProvider>().ActiveConfiguration; var config = c.Resolve<IConfigurationProvider>().ActiveConfiguration;

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flurl; using Flurl;
using Flurl.Http; using Flurl.Http;
using TrashLib.Config;
using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Api.Objects;
namespace TrashLib.Sonarr.Api namespace TrashLib.Sonarr.Api

@ -0,0 +1,9 @@
namespace TrashLib.Sonarr.Config
{
public interface ISonarrValidationMessages
{
string BaseUrl { get; }
string ApiKey { get; }
string ReleaseProfileType { get; }
}
}

@ -1,33 +1,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.QualityDefinition;
using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile;
namespace TrashLib.Sonarr namespace TrashLib.Sonarr.Config
{ {
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class SonarrConfiguration : ServiceConfiguration public class SonarrConfiguration : ServiceConfiguration
{ {
public IList<ReleaseProfileConfig> ReleaseProfiles { get; set; } = new List<ReleaseProfileConfig>(); public IList<ReleaseProfileConfig> ReleaseProfiles { get; set; } = new List<ReleaseProfileConfig>();
public SonarrQualityDefinitionType? QualityDefinition { get; init; } public SonarrQualityDefinitionType? QualityDefinition { get; init; }
public override bool IsValid(out string msg)
{
msg = "";
return true;
}
} }
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class ReleaseProfileConfig 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 // -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 // all of this craziness is to avoid making the enum type nullable which will make using the property
// frustrating. // frustrating.
[EnumDataType(typeof(ReleaseProfileType),
ErrorMessage = "'type' is required for 'release_profiles' elements")]
public ReleaseProfileType Type { get; init; } = (ReleaseProfileType) (-1); public ReleaseProfileType Type { get; init; } = (ReleaseProfileType) (-1);
public bool StrictNegativeScores { get; init; } public bool StrictNegativeScores { get; init; }
@ -35,7 +23,6 @@ namespace TrashLib.Sonarr
public ICollection<string> Tags { get; init; } = new List<string>(); public ICollection<string> Tags { get; init; } = new List<string>();
} }
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class SonarrProfileFilterConfig public class SonarrProfileFilterConfig
{ {
public bool IncludeOptional { get; set; } public bool IncludeOptional { get; set; }

@ -0,0 +1,27 @@
using FluentValidation;
using JetBrains.Annotations;
namespace TrashLib.Sonarr.Config
{
[UsedImplicitly]
internal class SonarrConfigurationValidator : AbstractValidator<SonarrConfiguration>
{
public SonarrConfigurationValidator(
ISonarrValidationMessages messages,
IValidator<ReleaseProfileConfig> 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<ReleaseProfileConfig>
{
public ReleaseProfileConfigValidator(ISonarrValidationMessages messages)
{
RuleFor(x => x.Type).IsInEnum().WithMessage(messages.ReleaseProfileType);
}
}
}

@ -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";
}
}

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.QualityDefinition namespace TrashLib.Sonarr.QualityDefinition
{ {

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Serilog; using Serilog;
using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api;
using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Api.Objects;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.QualityDefinition namespace TrashLib.Sonarr.QualityDefinition
{ {

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.ReleaseProfile namespace TrashLib.Sonarr.ReleaseProfile
{ {

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.ReleaseProfile namespace TrashLib.Sonarr.ReleaseProfile
{ {

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.ReleaseProfile namespace TrashLib.Sonarr.ReleaseProfile
{ {

@ -7,6 +7,7 @@ using Common.Extensions;
using Flurl; using Flurl;
using Flurl.Http; using Flurl.Http;
using Serilog; using Serilog;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.ReleaseProfile namespace TrashLib.Sonarr.ReleaseProfile
{ {

@ -7,6 +7,7 @@ using Serilog;
using TrashLib.ExceptionTypes; using TrashLib.ExceptionTypes;
using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api;
using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Api.Objects;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.ReleaseProfile namespace TrashLib.Sonarr.ReleaseProfile
{ {

@ -1,5 +1,6 @@
using Autofac; using Autofac;
using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api;
using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.QualityDefinition;
using TrashLib.Sonarr.ReleaseProfile; using TrashLib.Sonarr.ReleaseProfile;
@ -10,6 +11,7 @@ namespace TrashLib.Sonarr
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
builder.RegisterType<SonarrApi>().As<ISonarrApi>(); builder.RegisterType<SonarrApi>().As<ISonarrApi>();
builder.RegisterType<SonarrValidationMessages>().As<ISonarrValidationMessages>();
// Release Profile Support // Release Profile Support
builder.RegisterType<ReleaseProfileUpdater>().As<IReleaseProfileUpdater>(); builder.RegisterType<ReleaseProfileUpdater>().As<IReleaseProfileUpdater>();

@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac.Extras.AggregateService" />
<PackageReference Include="Autofac" /> <PackageReference Include="Autofac" />
<PackageReference Include="Flurl.Http" /> <PackageReference Include="Autofac.Extras.AggregateService" />
<PackageReference Include="FluentValidation" />
<PackageReference Include="Flurl" /> <PackageReference Include="Flurl" />
<PackageReference Include="morelinq" /> <PackageReference Include="Flurl.Http" />
<PackageReference Include="Serilog" /> <PackageReference Include="Serilog" />
<PackageReference Include="System.Data.HashFunction.FNV" /> <PackageReference Include="System.Data.HashFunction.FNV" />
<PackageReference Include="System.IO.Abstractions" /> <PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="morelinq" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -19,7 +19,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csp
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "Common.Tests\Common.Tests.csproj", "{0720939D-1CA6-43D7-BBED-F8F894C4F562}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "Common.Tests\Common.Tests.csproj", "{0720939D-1CA6-43D7-BBED-F8F894C4F562}"
EndProject 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrashLib", "TrashLib\TrashLib.csproj", "{4F6ACBA6-9A7D-487C-ACC1-787CCC90A381}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrashLib", "TrashLib\TrashLib.csproj", "{4F6ACBA6-9A7D-487C-ACC1-787CCC90A381}"
EndProject EndProject

Loading…
Cancel
Save