diff --git a/src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs b/src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs index c04aaad..a4a0bfc 100644 --- a/src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs +++ b/src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs @@ -11,19 +11,19 @@ namespace Spectre.Console.Tests.Data { } - public override ValidationResult Validate(ICommandParameterInfo info, object value) + public override ValidationResult Validate(CommandParameterContext context) { - if (value is int integer) + if (context.Value is int integer) { if (integer % 2 == 0) { return ValidationResult.Success(); } - return ValidationResult.Error($"Number is not even ({info?.PropertyName})."); + return ValidationResult.Error($"Number is not even ({context.Parameter.PropertyName})."); } - throw new InvalidOperationException($"Parameter is not a number ({info?.PropertyName})."); + throw new InvalidOperationException($"Parameter is not a number ({context.Parameter.PropertyName})."); } } } diff --git a/src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs b/src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs index 19d5c07..afb329a 100644 --- a/src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs +++ b/src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs @@ -11,19 +11,19 @@ namespace Spectre.Console.Tests.Data { } - public override ValidationResult Validate(ICommandParameterInfo info, object value) + public override ValidationResult Validate(CommandParameterContext context) { - if (value is int integer) + if (context.Value is int integer) { if (integer > 0) { return ValidationResult.Success(); } - return ValidationResult.Error($"Number is not greater than 0 ({info?.PropertyName})."); + return ValidationResult.Error($"Number is not greater than 0 ({context.Parameter.PropertyName})."); } - throw new InvalidOperationException($"Parameter is not a number ({info?.PropertyName})."); + throw new InvalidOperationException($"Parameter is not a number ({context.Parameter.PropertyName})."); } } } \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.ValueProviders.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.ValueProviders.cs new file mode 100644 index 0000000..0001c56 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.ValueProviders.cs @@ -0,0 +1,89 @@ +using System.Linq; +using Shouldly; +using Spectre.Console.Cli; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class ValueProviders + { + public sealed class ValueProviderSettings : CommandSettings + { + [CommandOption("-f|--foo ")] + [IntegerValueProvider(32)] + public int Foo { get; set; } + } + + public sealed class IntegerValueProvider : ParameterValueProviderAttribute + { + private readonly int _value; + + public IntegerValueProvider(int value) + { + _value = value; + } + + public override bool TryGetValue(CommandParameterContext context, out object result) + { + if (context.Parameter.ParameterType == typeof(int)) + { + if (context.Value == null) + { + result = _value; + return true; + } + } + + result = null; + return false; + } + } + + [Fact] + public void Should_Use_Provided_Value_If_No_Value_Was_Specified() + { + // Given + var app = new CommandAppTester(); + app.SetDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var result = app.Run(); + + // Then + result.Settings.ShouldBeOfType().And(settings => + { + settings.Foo.ShouldBe(32); + }); + } + + [Fact] + public void Should_Not_Override_Value_If_Value_Was_Specified() + { + // Given + var app = new CommandAppTester(); + app.SetDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var result = app.Run("--foo", "12"); + + // Then + result.Settings.ShouldBeOfType().And(settings => + { + settings.Foo.ShouldBe(12); + }); + } + } + } +} diff --git a/src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs b/src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs index 6aa4465..b297733 100644 --- a/src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs +++ b/src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs @@ -27,9 +27,8 @@ namespace Spectre.Console.Cli /// /// Validates the parameter value. /// - /// The parameter info. - /// The parameter value. + /// The parameter context. /// The validation result. - public abstract ValidationResult Validate(ICommandParameterInfo info, object? value); + public abstract ValidationResult Validate(CommandParameterContext context); } } \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Annotations/ParameterValueProviderAttribute.cs b/src/Spectre.Console/Cli/Annotations/ParameterValueProviderAttribute.cs new file mode 100644 index 0000000..8cf509e --- /dev/null +++ b/src/Spectre.Console/Cli/Annotations/ParameterValueProviderAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// An base class attribute used for parameter completion. + /// + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public abstract class ParameterValueProviderAttribute : Attribute + { + /// + /// Gets a value for the parameter. + /// + /// The parameter context. + /// The resulting value. + /// true if a value was provided; otherwise, false. + public abstract bool TryGetValue(CommandParameterContext context, out object? result); + } +} diff --git a/src/Spectre.Console/Cli/CommandParameterContext.cs b/src/Spectre.Console/Cli/CommandParameterContext.cs new file mode 100644 index 0000000..658f37a --- /dev/null +++ b/src/Spectre.Console/Cli/CommandParameterContext.cs @@ -0,0 +1,38 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a context for related operations. + /// + public sealed class CommandParameterContext + { + /// + /// Gets the parameter. + /// + public ICommandParameterInfo Parameter { get; } + + /// + /// Gets the type resolver. + /// + public ITypeResolver Resolver { get; } + + /// + /// Gets tje parameter value. + /// + public object? Value { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The parameter. + /// The type resolver. + /// The parameter value. + public CommandParameterContext(ICommandParameterInfo parameter, ITypeResolver resolver, object? value) + { + Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); + Resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + Value = value; + } + } +} diff --git a/src/Spectre.Console/Cli/ICommandParameterInfo.cs b/src/Spectre.Console/Cli/ICommandParameterInfo.cs index b8e3def..ff3cd9c 100644 --- a/src/Spectre.Console/Cli/ICommandParameterInfo.cs +++ b/src/Spectre.Console/Cli/ICommandParameterInfo.cs @@ -1,3 +1,5 @@ +using System; + namespace Spectre.Console.Cli { /// @@ -9,12 +11,17 @@ namespace Spectre.Console.Cli /// Gets the property name. /// /// The property name. - public abstract string PropertyName { get; } + public string PropertyName { get; } + + /// + /// Gets the parameter type. + /// + public Type ParameterType { get; } /// /// Gets the description. /// /// The description. - public abstract string? Description { get; } + public string? Description { get; } } } \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs b/src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs index 44a1d16..4954a25 100644 --- a/src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs +++ b/src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs @@ -18,6 +18,18 @@ namespace Spectre.Console.Cli // Process unmapped parameters. foreach (var parameter in tree.Unmapped) { + // Got a value provider? + if (parameter.ValueProvider != null) + { + var context = new CommandParameterContext(parameter, resolver, null); + if (parameter.ValueProvider.TryGetValue(context, out var result)) + { + lookup.SetValue(parameter, result); + CommandValidator.ValidateParameter(parameter, lookup, resolver); + continue; + } + } + if (parameter.IsFlagValue()) { // Set the flag value to an empty, not set instance. @@ -42,7 +54,7 @@ namespace Spectre.Console.Cli } binder.Bind(parameter, resolver, value); - CommandValidator.ValidateParameter(parameter, lookup); + CommandValidator.ValidateParameter(parameter, lookup, resolver); } else if (Nullable.GetUnderlyingType(parameter.ParameterType) != null || !parameter.ParameterType.IsValueType) @@ -88,7 +100,17 @@ namespace Spectre.Console.Cli } } - CommandValidator.ValidateParameter(mapped.Parameter, lookup); + // Got a value provider? + if (mapped.Parameter.ValueProvider != null) + { + var context = new CommandParameterContext(mapped.Parameter, resolver, mapped.Value); + if (mapped.Parameter.ValueProvider.TryGetValue(context, out var result)) + { + lookup.SetValue(mapped.Parameter, result); + } + } + + CommandValidator.ValidateParameter(mapped.Parameter, lookup, resolver); } tree = tree.Next; diff --git a/src/Spectre.Console/Cli/Internal/CommandValidator.cs b/src/Spectre.Console/Cli/Internal/CommandValidator.cs index 8be072c..6fabd31 100644 --- a/src/Spectre.Console/Cli/Internal/CommandValidator.cs +++ b/src/Spectre.Console/Cli/Internal/CommandValidator.cs @@ -23,12 +23,13 @@ namespace Spectre.Console.Cli } } - public static void ValidateParameter(CommandParameter parameter, CommandValueLookup settings) + public static void ValidateParameter(CommandParameter parameter, CommandValueLookup settings, ITypeResolver resolver) { var assignedValue = settings.GetValue(parameter); foreach (var validator in parameter.Validators) { - var validationResult = validator.Validate(parameter, assignedValue); + var context = new CommandParameterContext(parameter, resolver, assignedValue); + var validationResult = validator.Validate(context); if (!validationResult.Successful) { // If there is a error message specified in the parameter validator attribute, diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs index ceba760..455c95d 100644 --- a/src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs @@ -13,9 +13,10 @@ namespace Spectre.Console.Cli public CommandArgument( Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description, TypeConverterAttribute? converter, DefaultValueAttribute? defaultValue, - CommandArgumentAttribute argument, IEnumerable validators) + CommandArgumentAttribute argument, ParameterValueProviderAttribute? valueProvider, + IEnumerable validators) : base(parameterType, parameterKind, property, description, converter, defaultValue, - null, validators, argument.IsRequired) + null, valueProvider, validators, argument.IsRequired) { Value = argument.ValueName; Position = argument.Position; diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs index 76fc323..03eb493 100644 --- a/src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs @@ -182,6 +182,7 @@ namespace Spectre.Console.Cli var description = property.GetCustomAttribute(); var converter = property.GetCustomAttribute(); var deconstructor = property.GetCustomAttribute(); + var valueProvider = property.GetCustomAttribute(); var validators = property.GetCustomAttributes(true); var defaultValue = property.GetCustomAttribute(); @@ -194,7 +195,8 @@ namespace Spectre.Console.Cli return new CommandOption(property.PropertyType, kind, property, description?.Description, converter, deconstructor, - attribute, validators, defaultValue, attribute.ValueIsOptional); + attribute, valueProvider, validators, defaultValue, + attribute.ValueIsOptional); } private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute) @@ -202,6 +204,7 @@ namespace Spectre.Console.Cli var description = property.GetCustomAttribute(); var converter = property.GetCustomAttribute(); var defaultValue = property.GetCustomAttribute(); + var valueProvider = property.GetCustomAttribute(); var validators = property.GetCustomAttributes(true); var kind = GetParameterKind(property.PropertyType); @@ -209,7 +212,8 @@ namespace Spectre.Console.Cli return new CommandArgument( property.PropertyType, kind, property, description?.Description, converter, - defaultValue, attribute, validators); + defaultValue, attribute, valueProvider, + validators); } private static ParameterKind GetOptionKind( diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs index f008988..4da1f37 100644 --- a/src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs @@ -16,10 +16,11 @@ namespace Spectre.Console.Cli public CommandOption( Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description, TypeConverterAttribute? converter, PairDeconstructorAttribute? deconstructor, - CommandOptionAttribute optionAttribute, IEnumerable validators, + CommandOptionAttribute optionAttribute, ParameterValueProviderAttribute? valueProvider, + IEnumerable validators, DefaultValueAttribute? defaultValue, bool valueIsOptional) : base(parameterType, parameterKind, property, description, converter, - defaultValue, deconstructor, validators, false) + defaultValue, deconstructor, valueProvider, validators, false) { LongNames = optionAttribute.LongNames; ShortNames = optionAttribute.ShortNames; diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs index 30722b5..d99e4d1 100644 --- a/src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs @@ -17,6 +17,7 @@ namespace Spectre.Console.Cli public TypeConverterAttribute? Converter { get; } public PairDeconstructorAttribute? PairDeconstructor { get; } public List Validators { get; } + public ParameterValueProviderAttribute? ValueProvider { get; } public bool Required { get; set; } public string PropertyName => Property.Name; @@ -28,6 +29,7 @@ namespace Spectre.Console.Cli string? description, TypeConverterAttribute? converter, DefaultValueAttribute? defaultValue, PairDeconstructorAttribute? deconstuctor, + ParameterValueProviderAttribute? valueProvider, IEnumerable validators, bool required) { Id = Guid.NewGuid(); @@ -38,6 +40,7 @@ namespace Spectre.Console.Cli Converter = converter; DefaultValue = defaultValue; PairDeconstructor = deconstuctor; + ValueProvider = valueProvider; Validators = new List(validators ?? Array.Empty()); Required = required; } diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index 302031d..cbed974 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -28,10 +28,6 @@ - - - - 3.0.0 False