mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Custom value validators (#87)
This commit is contained in:
@@ -252,6 +252,26 @@ namespace CliFx.Tests
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_parameter_custom_validator_must_implement_the_corresponding_interface()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<InvalidCustomValidatorParameterCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_have_names_that_are_not_empty()
|
||||
{
|
||||
@@ -411,5 +431,25 @@ namespace CliFx.Tests
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_option_custom_validator_must_implement_the_corresponding_interface()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<InvalidCustomValidatorOptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command]
|
||||
public class InvalidCustomValidatorOptionCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption('f', Validators = new[] { typeof(Validator) })]
|
||||
public string? Option { get; set; }
|
||||
|
||||
public class Validator
|
||||
{
|
||||
public ValidationResult Validate(string value) => ValidationResult.Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command]
|
||||
public class InvalidCustomValidatorParameterCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandParameter(0, Validators = new[] { typeof(Validator) })]
|
||||
public string? Param { get; set; }
|
||||
|
||||
public class Validator
|
||||
{
|
||||
public ValidationResult Validate(string value) => ValidationResult.Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
CliFx/ArgumentValueValidator.cs
Normal file
18
CliFx/ArgumentValueValidator.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace CliFx
|
||||
{
|
||||
/// <summary>
|
||||
/// A base type for custom validators.
|
||||
/// </summary>
|
||||
public abstract class ArgumentValueValidator<T> : IArgumentValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Your validation logic have to be implemented in this method.
|
||||
/// </summary>
|
||||
public abstract ValidationResult Validate(T value);
|
||||
|
||||
/// <summary>
|
||||
/// Non-generic method, will be called by the framework.
|
||||
/// </summary>
|
||||
public ValidationResult Validate(object value) => Validate((T) value);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ namespace CliFx.Attributes
|
||||
/// </summary>
|
||||
public Type? Converter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of a converter to use for the option value evaluating.
|
||||
/// </summary>
|
||||
public Type[]? Validators { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -32,6 +32,11 @@ namespace CliFx.Attributes
|
||||
/// </summary>
|
||||
public Type? Converter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of a converter to use for the option value evaluating.
|
||||
/// </summary>
|
||||
public Type[]? Validators { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -19,11 +19,14 @@ namespace CliFx.Domain
|
||||
|
||||
public Type? ConverterType { get; }
|
||||
|
||||
protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converterType)
|
||||
public readonly Type[]? ValidatorTypes;
|
||||
|
||||
protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converterType = null, Type[]? validators = null)
|
||||
{
|
||||
Property = property;
|
||||
Description = description;
|
||||
ConverterType = converterType;
|
||||
ValidatorTypes = validators;
|
||||
}
|
||||
|
||||
private Type? TryGetEnumerableArgumentUnderlyingType() =>
|
||||
@@ -132,8 +135,15 @@ namespace CliFx.Domain
|
||||
}
|
||||
}
|
||||
|
||||
public void BindOn(ICommand command, IReadOnlyList<string> values) =>
|
||||
Property?.SetValue(command, Convert(values));
|
||||
public void BindOn(ICommand command, IReadOnlyList<string> values)
|
||||
{
|
||||
var value = Convert(values);
|
||||
|
||||
if (ValidatorTypes.NotEmpty())
|
||||
Validate(value);
|
||||
|
||||
Property?.SetValue(command, value);
|
||||
}
|
||||
|
||||
public void BindOn(ICommand command, params string[] values) =>
|
||||
BindOn(command, (IReadOnlyList<string>) values);
|
||||
@@ -153,6 +163,25 @@ namespace CliFx.Domain
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
private void Validate(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
|
||||
var failed = new List<ValidationResult>();
|
||||
foreach (var validator in ValidatorTypes!)
|
||||
{
|
||||
var result = validator.CreateInstance<IArgumentValueValidator>().Validate(value!);
|
||||
if (result.IsValid)
|
||||
continue;
|
||||
|
||||
failed.Add(result);
|
||||
}
|
||||
|
||||
if (failed.NotEmpty())
|
||||
throw CliFxException.ValueValidationFailed(this, failed.Select(x => x.ErrorMessage!));
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class CommandArgumentSchema
|
||||
|
||||
@@ -24,8 +24,9 @@ namespace CliFx.Domain
|
||||
string? environmentVariableName,
|
||||
bool isRequired,
|
||||
string? description,
|
||||
Type? converterType)
|
||||
: base(property, description, converterType)
|
||||
Type? converterType = null,
|
||||
Type[]? validatorTypes = null)
|
||||
: base(property, description, converterType, validatorTypes)
|
||||
{
|
||||
Name = name;
|
||||
ShortName = shortName;
|
||||
@@ -99,7 +100,8 @@ namespace CliFx.Domain
|
||||
attribute.EnvironmentVariableName,
|
||||
attribute.IsRequired,
|
||||
attribute.Description,
|
||||
attribute.Converter
|
||||
attribute.Converter,
|
||||
attribute.Validators
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -107,9 +109,9 @@ namespace CliFx.Domain
|
||||
internal partial class CommandOptionSchema
|
||||
{
|
||||
public static CommandOptionSchema HelpOption { get; } =
|
||||
new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text.", null);
|
||||
new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text.", converterType: null, validatorTypes: null);
|
||||
|
||||
public static CommandOptionSchema VersionOption { get; } =
|
||||
new CommandOptionSchema(null, "version", null, null, false, "Shows version information.", null);
|
||||
new CommandOptionSchema(null, "version", null, null, false, "Shows version information.", converterType: null, validatorTypes: null);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,9 @@ namespace CliFx.Domain
|
||||
int order,
|
||||
string name,
|
||||
string? description,
|
||||
Type? converterType)
|
||||
: base(property, description, converterType)
|
||||
Type? converterType = null,
|
||||
Type[]? validatorTypes = null)
|
||||
: base(property, description, converterType, validatorTypes)
|
||||
{
|
||||
Order = order;
|
||||
Name = name;
|
||||
@@ -57,7 +58,8 @@ namespace CliFx.Domain
|
||||
attribute.Order,
|
||||
name,
|
||||
attribute.Description,
|
||||
attribute.Converter
|
||||
attribute.Converter,
|
||||
attribute.Validators
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,18 @@ namespace CliFx.Domain
|
||||
invalidConverterParameters
|
||||
);
|
||||
}
|
||||
|
||||
var invalidValidatorParameters = command.Parameters
|
||||
.Where(p => p.ValidatorTypes != null && !p.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
|
||||
.ToArray();
|
||||
|
||||
if (invalidValidatorParameters.Any())
|
||||
{
|
||||
throw CliFxException.ParametersWithInvalidValidators(
|
||||
command,
|
||||
invalidValidatorParameters
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateOptions(CommandSchema command)
|
||||
@@ -208,6 +220,18 @@ namespace CliFx.Domain
|
||||
invalidConverterOptions
|
||||
);
|
||||
}
|
||||
|
||||
var invalidValidatorOptions = command.Options
|
||||
.Where(o => o.ValidatorTypes != null && !o.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
|
||||
.ToArray();
|
||||
|
||||
if (invalidValidatorOptions.Any())
|
||||
{
|
||||
throw CliFxException.OptionsWithInvalidValidators(
|
||||
command,
|
||||
invalidValidatorOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
|
||||
|
||||
@@ -185,6 +185,19 @@ Specified converter must implement {typeof(IArgumentValueConverter).FullName}.";
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException ParametersWithInvalidValidators(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameter(s) with invalid value validators:
|
||||
{invalidParameters.JoinToString(Environment.NewLine)}
|
||||
|
||||
Specified validator(s) must inherit from {typeof(ArgumentValueValidator<>).FullName}.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException OptionsWithNoName(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
@@ -269,6 +282,19 @@ Specified converter must implement {typeof(IArgumentValueConverter).FullName}.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException OptionsWithInvalidValidators(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} option(s) with invalid validators:
|
||||
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||
|
||||
Specified validators must inherit from {typeof(IArgumentValueValidator).FullName}.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
// End-user-facing exceptions
|
||||
@@ -415,5 +441,13 @@ Unrecognized options provided:
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException ValueValidationFailed(CommandArgumentSchema argument, IEnumerable<string> errors)
|
||||
{
|
||||
var message = $@"
|
||||
The validation of the provided value for {argument.Property!.Name} is failed because: {errors.JoinToString(Environment.NewLine)}";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
7
CliFx/IArgumentValueValidator.cs
Normal file
7
CliFx/IArgumentValueValidator.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace CliFx
|
||||
{
|
||||
internal interface IArgumentValueValidator
|
||||
{
|
||||
ValidationResult Validate(object value);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CliFx.Internal.Extensions
|
||||
{
|
||||
@@ -9,5 +10,11 @@ namespace CliFx.Internal.Extensions
|
||||
foreach (var item in items)
|
||||
source.Remove(item);
|
||||
}
|
||||
|
||||
public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source) =>
|
||||
!source?.Any() ?? true;
|
||||
|
||||
public static bool NotEmpty<T>(this IEnumerable<T>? source) =>
|
||||
!source.IsNullOrEmpty();
|
||||
}
|
||||
}
|
||||
30
CliFx/ValidationResult.cs
Normal file
30
CliFx/ValidationResult.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace CliFx
|
||||
{
|
||||
/// <summary>
|
||||
/// A tiny object that represents a result of the validation.
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// False if there is no error message, otherwise - true.
|
||||
/// </summary>
|
||||
public bool IsValid => ErrorMessage == null;
|
||||
|
||||
/// <summary>
|
||||
/// Contains an information about the reasons of failed validation.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; private set; }
|
||||
|
||||
private ValidationResult() { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates Ok result, means that the validation is passed.
|
||||
/// </summary>
|
||||
public static ValidationResult Ok() => new ValidationResult() { };
|
||||
|
||||
/// <summary>
|
||||
/// Creates Error result, means that the validation failed.
|
||||
/// </summary>
|
||||
public static ValidationResult Error(string message) => new ValidationResult() { ErrorMessage = message };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user