diff --git a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs index 132e2455..38054133 100644 --- a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs @@ -99,16 +99,16 @@ private void AddArgument(PropertyInfo prop, if (argument.MultipleValues) { - var collectionParser = CollectionParserProvider.Default.GetParser( - prop.PropertyType, - convention.Application.ValueParsers); - if (collectionParser == null) - { - throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); - } - convention.Application.OnParsingComplete(r => { + var collectionParser = CollectionParserProvider.Default.GetParser( + prop.PropertyType, + convention.Application.ValueParsers); + if (collectionParser == null) + { + throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); + } + if (argument.Values.Count == 0) { return; @@ -122,14 +122,14 @@ private void AddArgument(PropertyInfo prop, } else { - var parser = convention.Application.ValueParsers.GetParser(prop.PropertyType); - if (parser == null) - { - throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); - } - convention.Application.OnParsingComplete(r => { + var parser = convention.Application.ValueParsers.GetParser(prop.PropertyType); + if (parser == null) + { + throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); + } + if (argument.Values.Count == 0) { return; diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index db72a6f0..60f62688 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -62,34 +62,42 @@ private protected void AddOption(ConventionContext context, CommandOption option switch (option.OptionType) { case CommandOptionType.MultipleValue: - var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType, context.Application.ValueParsers); - if (collectionParser == null) - { - throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); - } context.Application.OnParsingComplete(_ => { + var collectionParser = + CollectionParserProvider.Default.GetParser(prop.PropertyType, + context.Application.ValueParsers); + if (collectionParser == null) + { + throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); + } + if (!option.HasValue()) { return; } + setter.Invoke(modelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values)); }); break; case CommandOptionType.SingleOrNoValue: case CommandOptionType.SingleValue: - var parser = context.Application.ValueParsers.GetParser(prop.PropertyType); - if (parser == null) - { - throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); - } context.Application.OnParsingComplete(_ => { + var parser = context.Application.ValueParsers.GetParser(prop.PropertyType); + if (parser == null) + { + throw new InvalidOperationException(Strings.CannotDetermineParserType(prop)); + } + if (!option.HasValue()) { return; } - setter.Invoke(modelAccessor.GetModel(), parser.Parse(option.LongName, option.Value(), context.Application.ValueParsers.ParseCulture)); + + setter.Invoke(modelAccessor.GetModel(), + parser.Parse(option.LongName, option.Value(), + context.Application.ValueParsers.ParseCulture)); }); break; case CommandOptionType.NoValue: diff --git a/test/Hosting.CommandLine.Tests/CustomValueParserTests.cs b/test/Hosting.CommandLine.Tests/CustomValueParserTests.cs new file mode 100644 index 00000000..9ab4a65d --- /dev/null +++ b/test/Hosting.CommandLine.Tests/CustomValueParserTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Threading.Tasks; +using McMaster.Extensions.CommandLineUtils; +using McMaster.Extensions.CommandLineUtils.Abstractions; +using McMaster.Extensions.CommandLineUtils.Conventions; +using McMaster.Extensions.Hosting.CommandLine.Tests.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Xunit.Abstractions; + +namespace McMaster.Extensions.Hosting.CommandLine.Tests +{ + public class CustomValueParserTests + { + private const string DemoOptionValue = "{\"Value\": \"TheValue\"}"; + + private readonly ITestOutputHelper _output; + + public CustomValueParserTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public async Task ItParsesUsingCustomParserFromConfigAction() + { + var exitCode = await new HostBuilder() + .ConfigureServices(collection => collection.AddSingleton(new TestConsole(_output))) + .RunCommandLineApplicationAsync( + new[] { "--custom-type", DemoOptionValue }, + app => app.ValueParsers.AddOrReplace( + new CustomValueParser())); + Assert.Equal(0, exitCode); + } + + [Fact] + public async Task ItParsesUsingCustomParserFromInjectedConvention() + { + var exitCode = await new HostBuilder() + .ConfigureServices(collection => + { + collection.AddSingleton(new TestConsole(_output)); + collection.AddSingleton(); + }) + .RunCommandLineApplicationAsync( + new[] { "--custom-type", DemoOptionValue }); + Assert.Equal(0, exitCode); + } + + [Fact] + public async Task ItParsesUsingCustomParserFromAttribute() + { + var exitCode = await new HostBuilder() + .ConfigureServices(collection => collection.AddSingleton(new TestConsole(_output))) + .RunCommandLineApplicationAsync( + new[] { "--custom-type", DemoOptionValue }); + Assert.Equal(0, exitCode); + } + + class CustomType + { + public string Value { get; set; } + } + + class CustomValueParser : IValueParser + { + public Type TargetType => typeof(CustomType); + + public CustomType Parse(string? argName, string? value, CultureInfo culture) + { + return JsonSerializer.Deserialize(value); + } + + object? IValueParser.Parse(string? argName, string? value, CultureInfo culture) + { + return Parse(argName, value, culture); + } + } + + [Command] + class CustomOptionTypeCommand + { + [Option("--custom-type", CommandOptionType.SingleValue)] + public CustomType Option { get; set; } + + private int OnExecute() + { + if (Option == null) + { + return 1; + } + + if (!"TheValue".Equals(Option.Value, StringComparison.Ordinal)) + { + return 2; + } + + return 0; + } + } + + class CustomValueParserConvention : IConvention + { + public void Apply(ConventionContext context) + { + context.Application.ValueParsers.AddOrReplace(new CustomValueParser()); + } + } + + [AttributeUsage(AttributeTargets.Class)] + class CustomValueParserConventionAttribute : Attribute, IConvention + { + public void Apply(ConventionContext context) + { + context.Application.ValueParsers.AddOrReplace(new CustomValueParser()); + } + } + + [Command] + [CustomValueParserConvention] + class CustomOptionTypeCommandWithAttribute + { + [Option("--custom-type", CommandOptionType.SingleValue)] + public CustomType Option { get; set; } + + private int OnExecute() + { + if (Option == null) + { + return 1; + } + + if (!"TheValue".Equals(Option.Value, StringComparison.Ordinal)) + { + return 2; + } + + return 0; + } + } + } +}