From 6a38c04c11c4cd83c419b8339a574352c9604b07 Mon Sep 17 00:00:00 2001 From: Oleksandr Shustov Date: Fri, 16 Oct 2020 14:22:42 +0300 Subject: [PATCH] Custom value converters (#81) --- CliFx.Tests/ArgumentConversionSpecs.cs | 66 +++++++++++++++++++ .../CommandWithParameterOfCustomType.cs | 27 ++++++++ .../Converters/CustomTypeConverter.cs | 8 +++ CliFx/Attributes/CommandOptionAttribute.cs | 5 ++ CliFx/Attributes/CommandParameterAttribute.cs | 5 ++ CliFx/Domain/CommandArgumentSchema.cs | 8 ++- CliFx/Domain/CommandOptionSchema.cs | 8 ++- CliFx/Domain/CommandParameterSchema.cs | 10 +-- CliFx/IArgumentValueConverter.cs | 13 ++++ CliFx/Internal/Extensions/TypeExtensions.cs | 5 ++ 10 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 CliFx.Tests/Commands/CommandWithParameterOfCustomType.cs create mode 100644 CliFx.Tests/Commands/Converters/CustomTypeConverter.cs create mode 100644 CliFx/IArgumentValueConverter.cs diff --git a/CliFx.Tests/ArgumentConversionSpecs.cs b/CliFx.Tests/ArgumentConversionSpecs.cs index 9efe00d..9be90e4 100644 --- a/CliFx.Tests/ArgumentConversionSpecs.cs +++ b/CliFx.Tests/ArgumentConversionSpecs.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using CliFx.Tests.Commands; +using CliFx.Tests.Commands.Converters; using CliFx.Tests.Internal; using FluentAssertions; using Xunit; @@ -1348,5 +1349,70 @@ namespace CliFx.Tests _output.WriteLine(stdErr.GetString()); } + + [Fact] + public async Task Property_of_custom_type_is_bound_when_the_valid_converter_type_is_specified() + { + // Arrange + const string foo = "foo"; + + var (console, stdOut, _) = VirtualConsole.CreateBuffered(); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--prop", foo + }); + + // Assert + exitCode.Should().Be(0); + + var commandInstance = stdOut.GetString().DeserializeJson(); + + commandInstance.Should().BeEquivalentTo(new CommandWithParameterOfCustomType() + { + MyProperty = (CustomType) new CustomTypeConverter().ConvertFrom(foo) + }); + } + + [Fact] + public async Task Enumerable_of_the_custom_type_is_bound_when_the_valid_converter_type_is_specified() + { + // Arrange + string foo = "foo"; + string bar = "bar"; + + var (console, stdOut, _) = VirtualConsole.CreateBuffered(); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--prop", foo, bar + }); + + // Assert + exitCode.Should().Be(0); + + var commandInstance = stdOut.GetString().DeserializeJson(); + + commandInstance.Should().BeEquivalentTo(new CommandWithEnumerableOfParametersOfCustomType() + { + MyProperties = new List + { + (CustomType) new CustomTypeConverter().ConvertFrom(foo), + (CustomType) new CustomTypeConverter().ConvertFrom(bar) + } + }); + } } } \ No newline at end of file diff --git a/CliFx.Tests/Commands/CommandWithParameterOfCustomType.cs b/CliFx.Tests/Commands/CommandWithParameterOfCustomType.cs new file mode 100644 index 0000000..ac1d14b --- /dev/null +++ b/CliFx.Tests/Commands/CommandWithParameterOfCustomType.cs @@ -0,0 +1,27 @@ +using CliFx.Attributes; +using System; +using System.Threading.Tasks; +using CliFx.Tests.Commands.Converters; +using System.Collections.Generic; + +namespace CliFx.Tests.Commands +{ + public class CustomType + { + public int SomeValue { get; set; } + } + + [Command("cmd")] + public class CommandWithParameterOfCustomType : SelfSerializeCommandBase + { + [CommandOption("prop", Converter = typeof(CustomTypeConverter))] + public CustomType? MyProperty { get; set; } + } + + [Command("cmd")] + public class CommandWithEnumerableOfParametersOfCustomType : SelfSerializeCommandBase + { + [CommandOption("prop", Converter = typeof(CustomTypeConverter))] + public List? MyProperties { get; set; } + } +} diff --git a/CliFx.Tests/Commands/Converters/CustomTypeConverter.cs b/CliFx.Tests/Commands/Converters/CustomTypeConverter.cs new file mode 100644 index 0000000..3b17a3f --- /dev/null +++ b/CliFx.Tests/Commands/Converters/CustomTypeConverter.cs @@ -0,0 +1,8 @@ +namespace CliFx.Tests.Commands.Converters +{ + public class CustomTypeConverter : IArgumentValueConverter + { + public object ConvertFrom(string value) => + new CustomType { SomeValue = value.Length }; + } +} diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs index 985c3e7..787ba53 100644 --- a/CliFx/Attributes/CommandOptionAttribute.cs +++ b/CliFx/Attributes/CommandOptionAttribute.cs @@ -37,6 +37,11 @@ namespace CliFx.Attributes /// public string? EnvironmentVariableName { get; set; } + /// + /// Type of a converter to use for the option value evaluating. + /// + public Type? Converter { get; set; } + /// /// Initializes an instance of . /// diff --git a/CliFx/Attributes/CommandParameterAttribute.cs b/CliFx/Attributes/CommandParameterAttribute.cs index 5ef1d27..67f5c87 100644 --- a/CliFx/Attributes/CommandParameterAttribute.cs +++ b/CliFx/Attributes/CommandParameterAttribute.cs @@ -26,6 +26,11 @@ namespace CliFx.Attributes /// public string? Description { get; set; } + /// + /// Type of a converter to use for the parameter value evaluating. + /// + public Type? Converter { get; set; } + /// /// Initializes an instance of . /// diff --git a/CliFx/Domain/CommandArgumentSchema.cs b/CliFx/Domain/CommandArgumentSchema.cs index 3dd0be1..dc726fb 100644 --- a/CliFx/Domain/CommandArgumentSchema.cs +++ b/CliFx/Domain/CommandArgumentSchema.cs @@ -17,10 +17,13 @@ namespace CliFx.Domain public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null; - protected CommandArgumentSchema(PropertyInfo? property, string? description) + protected Type? Converter { get; set; } + + protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converter = null) { Property = property; Description = description; + Converter = converter; } private Type? TryGetEnumerableArgumentUnderlyingType() => @@ -62,6 +65,9 @@ namespace CliFx.Domain var parseMethod = targetType.GetStaticParseMethod(); if (parseMethod != null) return parseMethod.Invoke(null, new object[] {value!}); + + if (Converter != null) + return Converter.InstanceOf().ConvertFrom(value!); } catch (Exception ex) { diff --git a/CliFx/Domain/CommandOptionSchema.cs b/CliFx/Domain/CommandOptionSchema.cs index 6bdafdc..f5476c3 100644 --- a/CliFx/Domain/CommandOptionSchema.cs +++ b/CliFx/Domain/CommandOptionSchema.cs @@ -23,8 +23,9 @@ namespace CliFx.Domain char? shortName, string? environmentVariableName, bool isRequired, - string? description) - : base(property, description) + string? description, + Type? converter = null) + : base(property, description, converter) { Name = name; ShortName = shortName; @@ -97,7 +98,8 @@ namespace CliFx.Domain attribute.ShortName, attribute.EnvironmentVariableName, attribute.IsRequired, - attribute.Description + attribute.Description, + attribute.Converter ); } } diff --git a/CliFx/Domain/CommandParameterSchema.cs b/CliFx/Domain/CommandParameterSchema.cs index c659f10..ea70137 100644 --- a/CliFx/Domain/CommandParameterSchema.cs +++ b/CliFx/Domain/CommandParameterSchema.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; using CliFx.Attributes; @@ -11,8 +12,8 @@ namespace CliFx.Domain public string Name { get; } - public CommandParameterSchema(PropertyInfo? property, int order, string name, string? description) - : base(property, description) + public CommandParameterSchema(PropertyInfo? property, int order, string name, string? description, Type? converter = null) + : base(property, description, converter) { Order = order; Name = name; @@ -50,7 +51,8 @@ namespace CliFx.Domain property, attribute.Order, name, - attribute.Description + attribute.Description, + attribute.Converter ); } } diff --git a/CliFx/IArgumentValueConverter.cs b/CliFx/IArgumentValueConverter.cs new file mode 100644 index 0000000..27c0c21 --- /dev/null +++ b/CliFx/IArgumentValueConverter.cs @@ -0,0 +1,13 @@ +namespace CliFx +{ + /// + /// Used as an interface for implementing custom parameter/option converters. + /// + public interface IArgumentValueConverter + { + /// + /// Converts an input value to object of required type. + /// + public object ConvertFrom(string value); + } +} diff --git a/CliFx/Internal/Extensions/TypeExtensions.cs b/CliFx/Internal/Extensions/TypeExtensions.cs index 6032bc2..ba289f1 100644 --- a/CliFx/Internal/Extensions/TypeExtensions.cs +++ b/CliFx/Internal/Extensions/TypeExtensions.cs @@ -56,5 +56,10 @@ namespace CliFx.Internal.Extensions return array; } + + public static T InstanceOf(this Type type) => + type.Implements(typeof(T)) + ? (T) Activator.CreateInstance(type) + : throw new ArgumentException(); } } \ No newline at end of file