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)
{
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)
{
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>
/// Validates the parameter value.
/// </summary>
/// <param name="info">The parameter info.</param>
/// <param name="value">The parameter value.</param>
/// <param name="context">The parameter context.</param>
/// <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
{
/// <summary>
@ -9,12 +11,17 @@ namespace Spectre.Console.Cli
/// Gets the property name.
/// </summary>
/// <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>
/// Gets the description.
/// </summary>
/// <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.
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;

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);
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,

View File

@ -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<ParameterValidationAttribute> validators)
CommandArgumentAttribute argument, ParameterValueProviderAttribute? valueProvider,
IEnumerable<ParameterValidationAttribute> validators)
: base(parameterType, parameterKind, property, description, converter, defaultValue,
null, validators, argument.IsRequired)
null, valueProvider, validators, argument.IsRequired)
{
Value = argument.ValueName;
Position = argument.Position;

View File

@ -182,6 +182,7 @@ namespace Spectre.Console.Cli
var description = property.GetCustomAttribute<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var deconstructor = property.GetCustomAttribute<PairDeconstructorAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true);
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
@ -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<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(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(

View File

@ -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<ParameterValidationAttribute> validators,
CommandOptionAttribute optionAttribute, ParameterValueProviderAttribute? valueProvider,
IEnumerable<ParameterValidationAttribute> 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;

View File

@ -17,6 +17,7 @@ namespace Spectre.Console.Cli
public TypeConverterAttribute? Converter { get; }
public PairDeconstructorAttribute? PairDeconstructor { get; }
public List<ParameterValidationAttribute> 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<ParameterValidationAttribute> 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<ParameterValidationAttribute>(validators ?? Array.Empty<ParameterValidationAttribute>());
Required = required;
}

View File

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