Skip to content

Commit

Permalink
fix: late bind parsers for attribute-discovered options & arguments (#…
Browse files Browse the repository at this point in the history
…398)

Currently the most reliable way to use a custom parser and still use property-attributes is implementing an attribute-class that also implements IConvention to register the custom parser, as UseAttributes is first in UseDefaultConventions.
  • Loading branch information
TheConstructor authored Nov 11, 2020
1 parent 4f686fe commit 760247d
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 25 deletions.
28 changes: 14 additions & 14 deletions src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
30 changes: 19 additions & 11 deletions src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
145 changes: 145 additions & 0 deletions test/Hosting.CommandLine.Tests/CustomValueParserTests.cs
Original file line number Diff line number Diff line change
@@ -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<IConsole>(new TestConsole(_output)))
.RunCommandLineApplicationAsync<CustomOptionTypeCommand>(
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<IConsole>(new TestConsole(_output));
collection.AddSingleton<IConvention, CustomValueParserConvention>();
})
.RunCommandLineApplicationAsync<CustomOptionTypeCommand>(
new[] { "--custom-type", DemoOptionValue });
Assert.Equal(0, exitCode);
}

[Fact]
public async Task ItParsesUsingCustomParserFromAttribute()
{
var exitCode = await new HostBuilder()
.ConfigureServices(collection => collection.AddSingleton<IConsole>(new TestConsole(_output)))
.RunCommandLineApplicationAsync<CustomOptionTypeCommandWithAttribute>(
new[] { "--custom-type", DemoOptionValue });
Assert.Equal(0, exitCode);
}

class CustomType
{
public string Value { get; set; }
}

class CustomValueParser : IValueParser<CustomType>
{
public Type TargetType => typeof(CustomType);

public CustomType Parse(string? argName, string? value, CultureInfo culture)
{
return JsonSerializer.Deserialize<CustomType>(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;
}
}
}
}

0 comments on commit 760247d

Please sign in to comment.