From b8c60717d5a2c6e25343a704f055b4ff1228b7bc Mon Sep 17 00:00:00 2001 From: Oleksandr Shustov Date: Fri, 6 Nov 2020 20:37:46 +0200 Subject: [PATCH] add a base type for custom validators --- CliFx/ArgumentValueValidator.cs | 18 ++++++++++ CliFx/Attributes/CommandOptionAttribute.cs | 5 +++ CliFx/Attributes/CommandParameterAttribute.cs | 5 +++ CliFx/Domain/CommandArgumentSchema.cs | 36 +++++++++++++++++-- CliFx/Domain/CommandOptionSchema.cs | 9 +++-- CliFx/Domain/CommandParameterSchema.cs | 14 ++++++-- CliFx/Exceptions/CliFxException.cs | 8 +++++ CliFx/IArgumentValueValidator.cs | 7 ++++ .../Extensions/CollectionExtensions.cs | 7 ++++ CliFx/ValidationResult.cs | 30 ++++++++++++++++ 10 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 CliFx/ArgumentValueValidator.cs create mode 100644 CliFx/IArgumentValueValidator.cs create mode 100644 CliFx/ValidationResult.cs diff --git a/CliFx/ArgumentValueValidator.cs b/CliFx/ArgumentValueValidator.cs new file mode 100644 index 0000000..69797bf --- /dev/null +++ b/CliFx/ArgumentValueValidator.cs @@ -0,0 +1,18 @@ +namespace CliFx +{ + /// + /// A base type for custom validators. + /// + public abstract class ArgumentValueValidator : IArgumentValueValidator + { + /// + /// Your validation logic have to be implemented in this method. + /// + public abstract ValidationResult Validate(T value); + + /// + /// Non-generic method, will be called by the framework. + /// + public ValidationResult Validate(object value) => Validate((T) value); + } +} diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs index 787ba53..66e9d61 100644 --- a/CliFx/Attributes/CommandOptionAttribute.cs +++ b/CliFx/Attributes/CommandOptionAttribute.cs @@ -42,6 +42,11 @@ namespace CliFx.Attributes /// public Type? Converter { get; set; } + /// + /// Type of a converter to use for the option value evaluating. + /// + public Type[]? Validators { get; set; } + /// /// Initializes an instance of . /// diff --git a/CliFx/Attributes/CommandParameterAttribute.cs b/CliFx/Attributes/CommandParameterAttribute.cs index 67f5c87..9e9bd7e 100644 --- a/CliFx/Attributes/CommandParameterAttribute.cs +++ b/CliFx/Attributes/CommandParameterAttribute.cs @@ -31,6 +31,11 @@ namespace CliFx.Attributes /// public Type? Converter { get; set; } + /// + /// Type of a converter to use for the option value evaluating. + /// + public Type[]? Validators { get; set; } + /// /// Initializes an instance of . /// diff --git a/CliFx/Domain/CommandArgumentSchema.cs b/CliFx/Domain/CommandArgumentSchema.cs index dc726fb..06dd49f 100644 --- a/CliFx/Domain/CommandArgumentSchema.cs +++ b/CliFx/Domain/CommandArgumentSchema.cs @@ -19,11 +19,15 @@ namespace CliFx.Domain protected Type? Converter { get; set; } - protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converter = null) + private readonly Type[]? _validators; + + protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converter = null, Type[]? validators = null) { Property = property; Description = description; Converter = converter; + + _validators = validators; } private Type? TryGetEnumerableArgumentUnderlyingType() => @@ -120,8 +124,15 @@ namespace CliFx.Domain } } - public void BindOn(ICommand command, IReadOnlyList values) => - Property?.SetValue(command, Convert(values)); + public void BindOn(ICommand command, IReadOnlyList values) + { + var value = Convert(values); + + if (_validators.NotEmpty()) + Validate(value); + + Property?.SetValue(command, value); + } public void BindOn(ICommand command, params string[] values) => BindOn(command, (IReadOnlyList) values); @@ -141,6 +152,25 @@ namespace CliFx.Domain return Array.Empty(); } + + private void Validate(object? value) + { + if (value is null) + return; + + var failed = new List(); + foreach (var validator in _validators!) + { + var result = validator.InstanceOf().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 diff --git a/CliFx/Domain/CommandOptionSchema.cs b/CliFx/Domain/CommandOptionSchema.cs index f5476c3..960b0e7 100644 --- a/CliFx/Domain/CommandOptionSchema.cs +++ b/CliFx/Domain/CommandOptionSchema.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -24,8 +25,9 @@ namespace CliFx.Domain string? environmentVariableName, bool isRequired, string? description, - Type? converter = null) - : base(property, description, converter) + Type? converter = null, + Type[]? validators = null) + : base(property, description, converter, validators) { Name = name; ShortName = shortName; @@ -99,7 +101,8 @@ namespace CliFx.Domain attribute.EnvironmentVariableName, attribute.IsRequired, attribute.Description, - attribute.Converter + attribute.Converter, + attribute.Validators ); } } diff --git a/CliFx/Domain/CommandParameterSchema.cs b/CliFx/Domain/CommandParameterSchema.cs index ea70137..7b6cbb5 100644 --- a/CliFx/Domain/CommandParameterSchema.cs +++ b/CliFx/Domain/CommandParameterSchema.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; @@ -12,8 +13,14 @@ namespace CliFx.Domain public string Name { get; } - public CommandParameterSchema(PropertyInfo? property, int order, string name, string? description, Type? converter = null) - : base(property, description, converter) + public CommandParameterSchema( + PropertyInfo? property, + int order, + string name, + string? description, + Type? converter = null, + Type[]? validators = null) + : base(property, description, converter, validators) { Order = order; Name = name; @@ -52,7 +59,8 @@ namespace CliFx.Domain attribute.Order, name, attribute.Description, - attribute.Converter + attribute.Converter, + attribute.Validators ); } } diff --git a/CliFx/Exceptions/CliFxException.cs b/CliFx/Exceptions/CliFxException.cs index f02c7f8..dfef45d 100644 --- a/CliFx/Exceptions/CliFxException.cs +++ b/CliFx/Exceptions/CliFxException.cs @@ -389,5 +389,13 @@ Unrecognized options provided: return new CliFxException(message.Trim()); } + + internal static CliFxException ValueValidationFailed(CommandArgumentSchema argument, IEnumerable 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()); + } } } \ No newline at end of file diff --git a/CliFx/IArgumentValueValidator.cs b/CliFx/IArgumentValueValidator.cs new file mode 100644 index 0000000..8c45b37 --- /dev/null +++ b/CliFx/IArgumentValueValidator.cs @@ -0,0 +1,7 @@ +namespace CliFx +{ + internal interface IArgumentValueValidator + { + ValidationResult Validate(object value); + } +} diff --git a/CliFx/Internal/Extensions/CollectionExtensions.cs b/CliFx/Internal/Extensions/CollectionExtensions.cs index 9ef38fd..38e29ec 100644 --- a/CliFx/Internal/Extensions/CollectionExtensions.cs +++ b/CliFx/Internal/Extensions/CollectionExtensions.cs @@ -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(this IEnumerable? source) => + !source?.Any() ?? true; + + public static bool NotEmpty(this IEnumerable? source) => + !source.IsNullOrEmpty(); } } \ No newline at end of file diff --git a/CliFx/ValidationResult.cs b/CliFx/ValidationResult.cs new file mode 100644 index 0000000..4a04f4e --- /dev/null +++ b/CliFx/ValidationResult.cs @@ -0,0 +1,30 @@ +namespace CliFx +{ + /// + /// A tiny object that represents a result of the validation. + /// + public class ValidationResult + { + /// + /// False if there is no error message, otherwise - true. + /// + public bool IsValid => ErrorMessage == null; + + /// + /// Contains an information about the reasons of failed validation. + /// + public string? ErrorMessage { get; private set; } + + private ValidationResult() { } + + /// + /// Creates Ok result, means that the validation is passed. + /// + public static ValidationResult Ok() => new ValidationResult() { }; + + /// + /// Creates Error result, means that the validation failed. + /// + public static ValidationResult Error(string message) => new ValidationResult() { ErrorMessage = message }; + } +}