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!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Trash\Trash.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>Trash.TestLibrary</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\TrashLib\TrashLib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
@ -1,4 +1,4 @@
|
|||||||
namespace TrashLib
|
namespace TrashLib.Config
|
||||||
{
|
{
|
||||||
public interface IServerInfo
|
public interface IServerInfo
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using Flurl;
|
using Flurl;
|
||||||
|
|
||||||
namespace TrashLib
|
namespace TrashLib.Config
|
||||||
{
|
{
|
||||||
internal class ServerInfo : IServerInfo
|
internal class ServerInfo : IServerInfo
|
||||||
{
|
{
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
@ -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'";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace TrashLib.Sonarr.Config
|
||||||
|
{
|
||||||
|
public interface ISonarrValidationMessages
|
||||||
|
{
|
||||||
|
string BaseUrl { get; }
|
||||||
|
string ApiKey { get; }
|
||||||
|
string ReleaseProfileType { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue