parent
1db23e6be9
commit
aecd2dc5dc
@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
</ItemGroup>
|
||||
</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.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<string> 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<ValidationFailure> validationFailures)
|
||||
: this(propertyName, deserializableType, validationFailures.Select(e => e.ToString()))
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using Flurl;
|
||||
|
||||
namespace TrashLib
|
||||
namespace TrashLib.Config
|
||||
{
|
||||
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