Add parameter value provider support

Adds support for parameter value providers which makes it
possible to set custom values for parameters.
This commit is contained in:
Patrik Svensson 2021-05-08 07:42:28 +02:00 committed by Phil Scott
parent d1d94cdebe
commit 1dd1945297
14 changed files with 208 additions and 27 deletions

View File

@ -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) if (integer % 2 == 0)
{ {
return ValidationResult.Success(); 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}).");
} }
} }
} }

View File

@ -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) if (integer > 0)
{ {
return ValidationResult.Success(); 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}).");
} }
} }
} }

View File

@ -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 <VALUE>")]
[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<GenericCommand<ValueProviderSettings>>();
app.Configure(config =>
{
config.PropagateExceptions();
});
// When
var result = app.Run();
// Then
result.Settings.ShouldBeOfType<ValueProviderSettings>().And(settings =>
{
settings.Foo.ShouldBe(32);
});
}
[Fact]
public void Should_Not_Override_Value_If_Value_Was_Specified()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<GenericCommand<ValueProviderSettings>>();
app.Configure(config =>
{
config.PropagateExceptions();
});
// When
var result = app.Run("--foo", "12");
// Then
result.Settings.ShouldBeOfType<ValueProviderSettings>().And(settings =>
{
settings.Foo.ShouldBe(12);
});
}
}
}
}

View File

@ -27,9 +27,8 @@ namespace Spectre.Console.Cli
/// <summary> /// <summary>
/// Validates the parameter value. /// Validates the parameter value.
/// </summary> /// </summary>
/// <param name="info">The parameter info.</param> /// <param name="context">The parameter context.</param>
/// <param name="value">The parameter value.</param>
/// <returns>The validation result.</returns> /// <returns>The validation result.</returns>
public abstract ValidationResult Validate(ICommandParameterInfo info, object? value); public abstract ValidationResult Validate(CommandParameterContext context);
} }
} }

View File

@ -0,0 +1,20 @@
using System;
namespace Spectre.Console.Cli
{
/// <summary>
/// An base class attribute used for parameter completion.
/// </summary>
/// <seealso cref="Attribute" />
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public abstract class ParameterValueProviderAttribute : Attribute
{
/// <summary>
/// Gets a value for the parameter.
/// </summary>
/// <param name="context">The parameter context.</param>
/// <param name="result">The resulting value.</param>
/// <returns><c>true</c> if a value was provided; otherwise, <c>false</c>.</returns>
public abstract bool TryGetValue(CommandParameterContext context, out object? result);
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace Spectre.Console.Cli
{
/// <summary>
/// Represents a context for <see cref="ICommandParameterInfo"/> related operations.
/// </summary>
public sealed class CommandParameterContext
{
/// <summary>
/// Gets the parameter.
/// </summary>
public ICommandParameterInfo Parameter { get; }
/// <summary>
/// Gets the type resolver.
/// </summary>
public ITypeResolver Resolver { get; }
/// <summary>
/// Gets tje parameter value.
/// </summary>
public object? Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CommandParameterContext"/> class.
/// </summary>
/// <param name="parameter">The parameter.</param>
/// <param name="resolver">The type resolver.</param>
/// <param name="value">The parameter value.</param>
public CommandParameterContext(ICommandParameterInfo parameter, ITypeResolver resolver, object? value)
{
Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter));
Resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
Value = value;
}
}
}

View File

@ -1,3 +1,5 @@
using System;
namespace Spectre.Console.Cli namespace Spectre.Console.Cli
{ {
/// <summary> /// <summary>
@ -9,12 +11,17 @@ namespace Spectre.Console.Cli
/// Gets the property name. /// Gets the property name.
/// </summary> /// </summary>
/// <value>The property name.</value> /// <value>The property name.</value>
public abstract string PropertyName { get; } public string PropertyName { get; }
/// <summary>
/// Gets the parameter type.
/// </summary>
public Type ParameterType { get; }
/// <summary> /// <summary>
/// Gets the description. /// Gets the description.
/// </summary> /// </summary>
/// <value>The description.</value> /// <value>The description.</value>
public abstract string? Description { get; } public string? Description { get; }
} }
} }

View File

@ -18,6 +18,18 @@ namespace Spectre.Console.Cli
// Process unmapped parameters. // Process unmapped parameters.
foreach (var parameter in tree.Unmapped) 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()) if (parameter.IsFlagValue())
{ {
// Set the flag value to an empty, not set instance. // Set the flag value to an empty, not set instance.
@ -42,7 +54,7 @@ namespace Spectre.Console.Cli
} }
binder.Bind(parameter, resolver, value); binder.Bind(parameter, resolver, value);
CommandValidator.ValidateParameter(parameter, lookup); CommandValidator.ValidateParameter(parameter, lookup, resolver);
} }
else if (Nullable.GetUnderlyingType(parameter.ParameterType) != null || else if (Nullable.GetUnderlyingType(parameter.ParameterType) != null ||
!parameter.ParameterType.IsValueType) !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; tree = tree.Next;

View File

@ -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); var assignedValue = settings.GetValue(parameter);
foreach (var validator in parameter.Validators) 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 (!validationResult.Successful)
{ {
// If there is a error message specified in the parameter validator attribute, // If there is a error message specified in the parameter validator attribute,

View File

@ -13,9 +13,10 @@ namespace Spectre.Console.Cli
public CommandArgument( public CommandArgument(
Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description, Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description,
TypeConverterAttribute? converter, DefaultValueAttribute? defaultValue, TypeConverterAttribute? converter, DefaultValueAttribute? defaultValue,
CommandArgumentAttribute argument, IEnumerable<ParameterValidationAttribute> validators) CommandArgumentAttribute argument, ParameterValueProviderAttribute? valueProvider,
IEnumerable<ParameterValidationAttribute> validators)
: base(parameterType, parameterKind, property, description, converter, defaultValue, : base(parameterType, parameterKind, property, description, converter, defaultValue,
null, validators, argument.IsRequired) null, valueProvider, validators, argument.IsRequired)
{ {
Value = argument.ValueName; Value = argument.ValueName;
Position = argument.Position; Position = argument.Position;

View File

@ -182,6 +182,7 @@ namespace Spectre.Console.Cli
var description = property.GetCustomAttribute<DescriptionAttribute>(); var description = property.GetCustomAttribute<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>(); var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var deconstructor = property.GetCustomAttribute<PairDeconstructorAttribute>(); var deconstructor = property.GetCustomAttribute<PairDeconstructorAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true); var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true);
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>(); var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
@ -194,7 +195,8 @@ namespace Spectre.Console.Cli
return new CommandOption(property.PropertyType, kind, return new CommandOption(property.PropertyType, kind,
property, description?.Description, converter, deconstructor, property, description?.Description, converter, deconstructor,
attribute, validators, defaultValue, attribute.ValueIsOptional); attribute, valueProvider, validators, defaultValue,
attribute.ValueIsOptional);
} }
private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute) private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute)
@ -202,6 +204,7 @@ namespace Spectre.Console.Cli
var description = property.GetCustomAttribute<DescriptionAttribute>(); var description = property.GetCustomAttribute<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>(); var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>(); var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true); var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true);
var kind = GetParameterKind(property.PropertyType); var kind = GetParameterKind(property.PropertyType);
@ -209,7 +212,8 @@ namespace Spectre.Console.Cli
return new CommandArgument( return new CommandArgument(
property.PropertyType, kind, property, property.PropertyType, kind, property,
description?.Description, converter, description?.Description, converter,
defaultValue, attribute, validators); defaultValue, attribute, valueProvider,
validators);
} }
private static ParameterKind GetOptionKind( private static ParameterKind GetOptionKind(

View File

@ -16,10 +16,11 @@ namespace Spectre.Console.Cli
public CommandOption( public CommandOption(
Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description, Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description,
TypeConverterAttribute? converter, PairDeconstructorAttribute? deconstructor, TypeConverterAttribute? converter, PairDeconstructorAttribute? deconstructor,
CommandOptionAttribute optionAttribute, IEnumerable<ParameterValidationAttribute> validators, CommandOptionAttribute optionAttribute, ParameterValueProviderAttribute? valueProvider,
IEnumerable<ParameterValidationAttribute> validators,
DefaultValueAttribute? defaultValue, bool valueIsOptional) DefaultValueAttribute? defaultValue, bool valueIsOptional)
: base(parameterType, parameterKind, property, description, converter, : base(parameterType, parameterKind, property, description, converter,
defaultValue, deconstructor, validators, false) defaultValue, deconstructor, valueProvider, validators, false)
{ {
LongNames = optionAttribute.LongNames; LongNames = optionAttribute.LongNames;
ShortNames = optionAttribute.ShortNames; ShortNames = optionAttribute.ShortNames;

View File

@ -17,6 +17,7 @@ namespace Spectre.Console.Cli
public TypeConverterAttribute? Converter { get; } public TypeConverterAttribute? Converter { get; }
public PairDeconstructorAttribute? PairDeconstructor { get; } public PairDeconstructorAttribute? PairDeconstructor { get; }
public List<ParameterValidationAttribute> Validators { get; } public List<ParameterValidationAttribute> Validators { get; }
public ParameterValueProviderAttribute? ValueProvider { get; }
public bool Required { get; set; } public bool Required { get; set; }
public string PropertyName => Property.Name; public string PropertyName => Property.Name;
@ -28,6 +29,7 @@ namespace Spectre.Console.Cli
string? description, TypeConverterAttribute? converter, string? description, TypeConverterAttribute? converter,
DefaultValueAttribute? defaultValue, DefaultValueAttribute? defaultValue,
PairDeconstructorAttribute? deconstuctor, PairDeconstructorAttribute? deconstuctor,
ParameterValueProviderAttribute? valueProvider,
IEnumerable<ParameterValidationAttribute> validators, bool required) IEnumerable<ParameterValidationAttribute> validators, bool required)
{ {
Id = Guid.NewGuid(); Id = Guid.NewGuid();
@ -38,6 +40,7 @@ namespace Spectre.Console.Cli
Converter = converter; Converter = converter;
DefaultValue = defaultValue; DefaultValue = defaultValue;
PairDeconstructor = deconstuctor; PairDeconstructor = deconstuctor;
ValueProvider = valueProvider;
Validators = new List<ParameterValidationAttribute>(validators ?? Array.Empty<ParameterValidationAttribute>()); Validators = new List<ParameterValidationAttribute>(validators ?? Array.Empty<ParameterValidationAttribute>());
Required = required; Required = required;
} }

View File

@ -28,10 +28,6 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Cli\" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<AnnotatedReferenceAssemblyVersion>3.0.0</AnnotatedReferenceAssemblyVersion> <AnnotatedReferenceAssemblyVersion>3.0.0</AnnotatedReferenceAssemblyVersion>
<GenerateNullableAttributes>False</GenerateNullableAttributes> <GenerateNullableAttributes>False</GenerateNullableAttributes>