Refactor recent PRs

This commit is contained in:
Tyrrrz
2020-11-18 18:00:43 +02:00
parent eba66d0878
commit d6da687170
14 changed files with 159 additions and 133 deletions

View File

@@ -28,7 +28,7 @@ namespace CliFx.Demo
public static async Task<int> Main() =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseTypeActivator(GetServiceProvider().GetService)
.UseTypeActivator(GetServiceProvider().GetRequiredService)
.Build()
.RunAsync();
}

View File

@@ -1,18 +1,55 @@
namespace CliFx
{
/// <summary>
/// A base type for custom validators.
/// Represents a result of a validation.
/// </summary>
public partial class ValidationResult
{
/// <summary>
/// Whether validation was successful.
/// </summary>
public bool IsValid => ErrorMessage == null;
/// <summary>
/// If validation has failed, contains the associated error, otherwise null.
/// </summary>
public string? ErrorMessage { get; }
/// <summary>
/// Initializes an instance of <see cref="ValidationResult"/>.
/// </summary>
public ValidationResult(string? errorMessage = null) =>
ErrorMessage = errorMessage;
}
public partial class ValidationResult
{
/// <summary>
/// Creates successful result, meaning that the validation has passed.
/// </summary>
public static ValidationResult Ok() => new ValidationResult();
/// <summary>
/// Creates an error result, meaning that the validation has failed.
/// </summary>
public static ValidationResult Error(string message) => new ValidationResult(message);
}
internal interface IArgumentValueValidator
{
ValidationResult Validate(object value);
}
/// <summary>
/// A base type for custom argument validators.
/// </summary>
public abstract class ArgumentValueValidator<T> : IArgumentValueValidator
{
/// <summary>
/// Your validation logic have to be implemented in this method.
/// Validates the value.
/// </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);
ValidationResult IArgumentValueValidator.Validate(object value) => Validate((T) value);
}
}

View File

@@ -0,0 +1,28 @@
using System;
namespace CliFx.Attributes
{
/// <summary>
/// Properties shared by parameter and option arguments.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public abstract class CommandArgumentAttribute : Attribute
{
/// <summary>
/// Option description, which is used in help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Type of converter to use when mapping the argument value.
/// Converter must implement <see cref="IArgumentValueConverter"/>.
/// </summary>
public Type? Converter { get; set; }
/// <summary>
/// Types of validators to use when mapping the argument value.
/// Validators must derive from <see cref="ArgumentValueValidator{T}"/>.
/// </summary>
public Type[] Validators { get; set; } = Array.Empty<Type>();
}
}

View File

@@ -6,7 +6,7 @@ namespace CliFx.Attributes
/// Annotates a property that defines a command option.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CommandOptionAttribute : Attribute
public class CommandOptionAttribute : CommandArgumentAttribute
{
/// <summary>
/// Option name (must be longer than a single character).
@@ -27,27 +27,11 @@ namespace CliFx.Attributes
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Option description, which is used in help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Environment variable that will be used as fallback if no option value is specified.
/// </summary>
public string? EnvironmentVariableName { get; set; }
/// <summary>
/// Type of converter to use when mapping the argument value.
/// Converter must implement <see cref="IArgumentValueConverter"/>.
/// </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>

View File

@@ -6,7 +6,7 @@ namespace CliFx.Attributes
/// Annotates a property that defines a command parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CommandParameterAttribute : Attribute
public class CommandParameterAttribute : CommandArgumentAttribute
{
/// <summary>
/// Order of this parameter compared to other parameters.
@@ -21,22 +21,6 @@ namespace CliFx.Attributes
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Parameter description, which is used in help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Type of converter to use when mapping the argument value.
/// Converter must implement <see cref="IArgumentValueConverter"/>.
/// </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>

View File

@@ -19,14 +19,18 @@ namespace CliFx.Domain
public Type? ConverterType { get; }
public readonly Type[]? ValidatorTypes;
public Type[] ValidatorTypes { get; }
protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converterType = null, Type[]? validators = null)
protected CommandArgumentSchema(
PropertyInfo? property,
string? description,
Type? converterType,
Type[] validatorTypes)
{
Property = property;
Description = description;
ConverterType = converterType;
ValidatorTypes = validators;
ValidatorTypes = validatorTypes;
}
private Type? TryGetEnumerableArgumentUnderlyingType() =>
@@ -135,12 +139,28 @@ namespace CliFx.Domain
}
}
private void Validate(object? value)
{
if (value is null)
return;
var validators = ValidatorTypes
.Select(t => t.CreateInstance<IArgumentValueValidator>())
.ToArray();
var failedValidations = validators
.Select(v => v.Validate(value))
.Where(result => !result.IsValid)
.ToArray();
if (failedValidations.Any())
throw CliFxException.ValidationFailed(this, failedValidations);
}
public void BindOn(ICommand command, IReadOnlyList<string> values)
{
var value = Convert(values);
if (ValidatorTypes.NotEmpty())
Validate(value);
Validate(value);
Property?.SetValue(command, value);
}
@@ -163,25 +183,6 @@ 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

View File

@@ -24,8 +24,8 @@ namespace CliFx.Domain
string? environmentVariableName,
bool isRequired,
string? description,
Type? converterType = null,
Type[]? validatorTypes = null)
Type? converterType,
Type[] validatorTypes)
: base(property, description, converterType, validatorTypes)
{
Name = name;
@@ -108,10 +108,26 @@ namespace CliFx.Domain
internal partial class CommandOptionSchema
{
public static CommandOptionSchema HelpOption { get; } =
new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text.", converterType: null, validatorTypes: null);
public static CommandOptionSchema HelpOption { get; } = new CommandOptionSchema(
null,
"help",
'h',
null,
false,
"Shows help text.",
null,
Array.Empty<Type>()
);
public static CommandOptionSchema VersionOption { get; } =
new CommandOptionSchema(null, "version", null, null, false, "Shows version information.", converterType: null, validatorTypes: null);
public static CommandOptionSchema VersionOption { get; } = new CommandOptionSchema(
null,
"version",
null,
null,
false,
"Shows version information.",
null,
Array.Empty<Type>()
);
}
}

View File

@@ -17,8 +17,8 @@ namespace CliFx.Domain
int order,
string name,
string? description,
Type? converterType = null,
Type[]? validatorTypes = null)
Type? converterType,
Type[] validatorTypes)
: base(property, description, converterType, validatorTypes)
{
Order = order;

View File

@@ -283,7 +283,7 @@ namespace CliFx.Domain
}
}
private void WriteCommandChildren(
private void WriteCommandDescendants(
CommandSchema command,
IReadOnlyList<CommandSchema> descendantCommands)
{
@@ -297,12 +297,7 @@ namespace CliFx.Domain
foreach (var descendantCommand in descendantCommands)
{
var relativeCommandName = !string.IsNullOrWhiteSpace(command.Name)
? descendantCommand.Name!.Substring(command.Name.Length).Trim()
: descendantCommand.Name!;
// Description
if (!string.IsNullOrWhiteSpace(descendantCommand.Description))
{
WriteHorizontalMargin();
@@ -310,7 +305,7 @@ namespace CliFx.Domain
WriteVerticalMargin();
}
// Name
// Usage
WriteHorizontalMargin(4);
WriteCommandUsageLineItem(descendantCommand, false);
@@ -355,7 +350,7 @@ namespace CliFx.Domain
WriteCommandUsage(command, descendantCommands);
WriteCommandParameters(command);
WriteCommandOptions(command, defaultValues);
WriteCommandChildren(command, descendantCommands);
WriteCommandDescendants(command, descendantCommands);
}
}

View File

@@ -128,7 +128,7 @@ namespace CliFx.Domain
}
var invalidValidatorParameters = command.Parameters
.Where(p => p.ValidatorTypes != null && !p.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
.Where(p => !p.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
.ToArray();
if (invalidValidatorParameters.Any())
@@ -222,7 +222,7 @@ namespace CliFx.Domain
}
var invalidValidatorOptions = command.Options
.Where(o => o.ValidatorTypes != null && !o.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
.Where(o => !o.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
.ToArray();
if (invalidValidatorOptions.Any())

View File

@@ -36,7 +36,8 @@ namespace CliFx.Exceptions
{
internal static CliFxException DefaultActivatorFailed(Type type, Exception? innerException = null)
{
var configureActivatorMethodName = $"{nameof(CliApplicationBuilder)}.{nameof(CliApplicationBuilder.UseTypeActivator)}(...)";
var configureActivatorMethodName =
$"{nameof(CliApplicationBuilder)}.{nameof(CliApplicationBuilder.UseTypeActivator)}(...)";
var message = $@"
Failed to create an instance of type '{type.FullName}'.
@@ -193,7 +194,7 @@ Specified converter must implement {typeof(IArgumentValueConverter).FullName}.";
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}.";
Specified validators must inherit from {typeof(ArgumentValueValidator<>).FullName}.";
return new CliFxException(message.Trim());
}
@@ -424,7 +425,8 @@ Missing values for one or more required options:
return new CliFxException(message.Trim());
}
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandParameterInput> parameterInputs)
internal static CliFxException UnrecognizedParametersProvided(
IReadOnlyList<CommandParameterInput> parameterInputs)
{
var message = $@"
Unrecognized parameters provided:
@@ -442,12 +444,35 @@ Unrecognized options provided:
return new CliFxException(message.Trim());
}
internal static CliFxException ValueValidationFailed(CommandArgumentSchema argument, IEnumerable<string> errors)
internal static CliFxException ValidationFailed(
CommandParameterSchema parameter,
IReadOnlyList<ValidationResult> failedResults)
{
var message = $@"
The validation of the provided value for {argument.Property!.Name} is failed because: {errors.JoinToString(Environment.NewLine)}";
Value provided for parameter {parameter.GetUserFacingDisplayString()}:
{failedResults.Select(r => r.ErrorMessage).JoinToString(Environment.NewLine)}";
return new CliFxException(message.Trim());
}
internal static CliFxException ValidationFailed(
CommandOptionSchema option,
IReadOnlyList<ValidationResult> failedResults)
{
var message = $@"
Value provided for option {option.GetUserFacingDisplayString()}:
{failedResults.Select(r => r.ErrorMessage).JoinToString(Environment.NewLine)}";
return new CliFxException(message.Trim());
}
internal static CliFxException ValidationFailed(
CommandArgumentSchema argument,
IReadOnlyList<ValidationResult> failedResults) => argument switch
{
CommandParameterSchema parameter => ValidationFailed(parameter, failedResults),
CommandOptionSchema option => ValidationFailed(option, failedResults),
_ => throw new ArgumentOutOfRangeException(nameof(argument))
};
}
}

View File

@@ -1,7 +0,0 @@
namespace CliFx
{
internal interface IArgumentValueValidator
{
ValidationResult Validate(object value);
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
namespace CliFx.Internal.Extensions
{
@@ -10,11 +9,5 @@ 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();
}
}

View File

@@ -1,30 +0,0 @@
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 };
}
}