Skip to content

Commit 760247d

Browse files
fix: late bind parsers for attribute-discovered options & arguments (#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.
1 parent 4f686fe commit 760247d

File tree

3 files changed

+178
-25
lines changed

3 files changed

+178
-25
lines changed

src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,16 @@ private void AddArgument(PropertyInfo prop,
9999

100100
if (argument.MultipleValues)
101101
{
102-
var collectionParser = CollectionParserProvider.Default.GetParser(
103-
prop.PropertyType,
104-
convention.Application.ValueParsers);
105-
if (collectionParser == null)
106-
{
107-
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
108-
}
109-
110102
convention.Application.OnParsingComplete(r =>
111103
{
104+
var collectionParser = CollectionParserProvider.Default.GetParser(
105+
prop.PropertyType,
106+
convention.Application.ValueParsers);
107+
if (collectionParser == null)
108+
{
109+
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
110+
}
111+
112112
if (argument.Values.Count == 0)
113113
{
114114
return;
@@ -122,14 +122,14 @@ private void AddArgument(PropertyInfo prop,
122122
}
123123
else
124124
{
125-
var parser = convention.Application.ValueParsers.GetParser(prop.PropertyType);
126-
if (parser == null)
127-
{
128-
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
129-
}
130-
131125
convention.Application.OnParsingComplete(r =>
132126
{
127+
var parser = convention.Application.ValueParsers.GetParser(prop.PropertyType);
128+
if (parser == null)
129+
{
130+
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
131+
}
132+
133133
if (argument.Values.Count == 0)
134134
{
135135
return;

src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,34 +62,42 @@ private protected void AddOption(ConventionContext context, CommandOption option
6262
switch (option.OptionType)
6363
{
6464
case CommandOptionType.MultipleValue:
65-
var collectionParser = CollectionParserProvider.Default.GetParser(prop.PropertyType, context.Application.ValueParsers);
66-
if (collectionParser == null)
67-
{
68-
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
69-
}
7065
context.Application.OnParsingComplete(_ =>
7166
{
67+
var collectionParser =
68+
CollectionParserProvider.Default.GetParser(prop.PropertyType,
69+
context.Application.ValueParsers);
70+
if (collectionParser == null)
71+
{
72+
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
73+
}
74+
7275
if (!option.HasValue())
7376
{
7477
return;
7578
}
79+
7680
setter.Invoke(modelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values));
7781
});
7882
break;
7983
case CommandOptionType.SingleOrNoValue:
8084
case CommandOptionType.SingleValue:
81-
var parser = context.Application.ValueParsers.GetParser(prop.PropertyType);
82-
if (parser == null)
83-
{
84-
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
85-
}
8685
context.Application.OnParsingComplete(_ =>
8786
{
87+
var parser = context.Application.ValueParsers.GetParser(prop.PropertyType);
88+
if (parser == null)
89+
{
90+
throw new InvalidOperationException(Strings.CannotDetermineParserType(prop));
91+
}
92+
8893
if (!option.HasValue())
8994
{
9095
return;
9196
}
92-
setter.Invoke(modelAccessor.GetModel(), parser.Parse(option.LongName, option.Value(), context.Application.ValueParsers.ParseCulture));
97+
98+
setter.Invoke(modelAccessor.GetModel(),
99+
parser.Parse(option.LongName, option.Value(),
100+
context.Application.ValueParsers.ParseCulture));
93101
});
94102
break;
95103
case CommandOptionType.NoValue:
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Text.Json;
4+
using System.Threading.Tasks;
5+
using McMaster.Extensions.CommandLineUtils;
6+
using McMaster.Extensions.CommandLineUtils.Abstractions;
7+
using McMaster.Extensions.CommandLineUtils.Conventions;
8+
using McMaster.Extensions.Hosting.CommandLine.Tests.Utilities;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Hosting;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
14+
namespace McMaster.Extensions.Hosting.CommandLine.Tests
15+
{
16+
public class CustomValueParserTests
17+
{
18+
private const string DemoOptionValue = "{\"Value\": \"TheValue\"}";
19+
20+
private readonly ITestOutputHelper _output;
21+
22+
public CustomValueParserTests(ITestOutputHelper output)
23+
{
24+
_output = output;
25+
}
26+
27+
[Fact]
28+
public async Task ItParsesUsingCustomParserFromConfigAction()
29+
{
30+
var exitCode = await new HostBuilder()
31+
.ConfigureServices(collection => collection.AddSingleton<IConsole>(new TestConsole(_output)))
32+
.RunCommandLineApplicationAsync<CustomOptionTypeCommand>(
33+
new[] { "--custom-type", DemoOptionValue },
34+
app => app.ValueParsers.AddOrReplace(
35+
new CustomValueParser()));
36+
Assert.Equal(0, exitCode);
37+
}
38+
39+
[Fact]
40+
public async Task ItParsesUsingCustomParserFromInjectedConvention()
41+
{
42+
var exitCode = await new HostBuilder()
43+
.ConfigureServices(collection =>
44+
{
45+
collection.AddSingleton<IConsole>(new TestConsole(_output));
46+
collection.AddSingleton<IConvention, CustomValueParserConvention>();
47+
})
48+
.RunCommandLineApplicationAsync<CustomOptionTypeCommand>(
49+
new[] { "--custom-type", DemoOptionValue });
50+
Assert.Equal(0, exitCode);
51+
}
52+
53+
[Fact]
54+
public async Task ItParsesUsingCustomParserFromAttribute()
55+
{
56+
var exitCode = await new HostBuilder()
57+
.ConfigureServices(collection => collection.AddSingleton<IConsole>(new TestConsole(_output)))
58+
.RunCommandLineApplicationAsync<CustomOptionTypeCommandWithAttribute>(
59+
new[] { "--custom-type", DemoOptionValue });
60+
Assert.Equal(0, exitCode);
61+
}
62+
63+
class CustomType
64+
{
65+
public string Value { get; set; }
66+
}
67+
68+
class CustomValueParser : IValueParser<CustomType>
69+
{
70+
public Type TargetType => typeof(CustomType);
71+
72+
public CustomType Parse(string? argName, string? value, CultureInfo culture)
73+
{
74+
return JsonSerializer.Deserialize<CustomType>(value);
75+
}
76+
77+
object? IValueParser.Parse(string? argName, string? value, CultureInfo culture)
78+
{
79+
return Parse(argName, value, culture);
80+
}
81+
}
82+
83+
[Command]
84+
class CustomOptionTypeCommand
85+
{
86+
[Option("--custom-type", CommandOptionType.SingleValue)]
87+
public CustomType Option { get; set; }
88+
89+
private int OnExecute()
90+
{
91+
if (Option == null)
92+
{
93+
return 1;
94+
}
95+
96+
if (!"TheValue".Equals(Option.Value, StringComparison.Ordinal))
97+
{
98+
return 2;
99+
}
100+
101+
return 0;
102+
}
103+
}
104+
105+
class CustomValueParserConvention : IConvention
106+
{
107+
public void Apply(ConventionContext context)
108+
{
109+
context.Application.ValueParsers.AddOrReplace(new CustomValueParser());
110+
}
111+
}
112+
113+
[AttributeUsage(AttributeTargets.Class)]
114+
class CustomValueParserConventionAttribute : Attribute, IConvention
115+
{
116+
public void Apply(ConventionContext context)
117+
{
118+
context.Application.ValueParsers.AddOrReplace(new CustomValueParser());
119+
}
120+
}
121+
122+
[Command]
123+
[CustomValueParserConvention]
124+
class CustomOptionTypeCommandWithAttribute
125+
{
126+
[Option("--custom-type", CommandOptionType.SingleValue)]
127+
public CustomType Option { get; set; }
128+
129+
private int OnExecute()
130+
{
131+
if (Option == null)
132+
{
133+
return 1;
134+
}
135+
136+
if (!"TheValue".Equals(Option.Value, StringComparison.Ordinal))
137+
{
138+
return 2;
139+
}
140+
141+
return 0;
142+
}
143+
}
144+
}
145+
}

0 commit comments

Comments
 (0)