Skip to content

Commit e50a75d

Browse files
authored
Merge pull request #736 from jonsequitur/fix-723
Consolidate argument default values, custom conversion, and validation
2 parents ae5e6fc + caa33a7 commit e50a75d

16 files changed

+315
-102
lines changed

src/System.CommandLine.Tests/ArgumentTests.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Collections.Generic;
5+
using System.CommandLine.Parsing;
6+
using System.IO;
47
using FluentAssertions;
8+
using System.Linq;
59
using Xunit;
610

711
namespace System.CommandLine.Tests
@@ -50,6 +54,142 @@ public void When_there_is_no_default_value_then_GetDefaultValue_throws()
5054
.Be("Argument \"the-arg\" does not have a default value");
5155
}
5256

57+
public class CustomParsing
58+
{
59+
[Fact]
60+
public void HasDefaultValue_can_be_set_to_true()
61+
{
62+
var argument = new Argument<FileSystemInfo>(result => null, true);
63+
64+
argument.HasDefaultValue
65+
.Should()
66+
.BeTrue();
67+
}
68+
69+
[Fact]
70+
public void HasDefaultValue_can_be_set_to_false()
71+
{
72+
var argument = new Argument<FileSystemInfo>(result => null, false);
73+
74+
argument.HasDefaultValue
75+
.Should()
76+
.BeFalse();
77+
}
78+
79+
[Fact]
80+
public void GetDefaultValue_returns_specified_value()
81+
{
82+
var argument = new Argument<string>(result => "the-default", isDefault: true);
83+
84+
argument.GetDefaultValue()
85+
.Should()
86+
.Be("the-default");
87+
}
88+
89+
[Fact]
90+
public void GetDefaultValue_returns_null_when_parse_delegate_returns_true_without_setting_a_value()
91+
{
92+
var argument = new Argument<string>(result => null, isDefault: true);
93+
94+
argument.GetDefaultValue()
95+
.Should()
96+
.BeNull();
97+
}
98+
99+
[Fact]
100+
public void GetDefaultValue_returns_null_when_parse_delegate_returns_true_and_sets_value_to_null()
101+
{
102+
var argument = new Argument<string>(result => null, isDefault: true);
103+
104+
argument.GetDefaultValue()
105+
.Should()
106+
.BeNull();
107+
}
108+
109+
[Fact]
110+
public void GetDefaultValue_can_return_null()
111+
{
112+
var argument = new Argument<string>(result => null, isDefault: true);
113+
114+
argument.GetDefaultValue()
115+
.Should()
116+
.BeNull();
117+
}
118+
119+
[Fact]
120+
public void validation_failure_message()
121+
{
122+
var argument = new Argument<FileSystemInfo>(result =>
123+
{
124+
result.ErrorMessage = "oops!";
125+
return null;
126+
});
127+
128+
argument.Parse("x")
129+
.Errors
130+
.Should()
131+
.ContainSingle(e => e.SymbolResult.Symbol == argument)
132+
.Which
133+
.Message
134+
.Should()
135+
.Be("oops!");
136+
}
137+
138+
[Fact]
139+
public void custom_parsing_of_scalar_value_from_an_argument_with_one_token()
140+
{
141+
var argument = new Argument<int>(result => int.Parse(result.Tokens.Single().Value));
142+
143+
argument.Parse("123")
144+
.FindResultFor(argument)
145+
.GetValueOrDefault()
146+
.Should()
147+
.Be(123);
148+
}
149+
150+
[Fact]
151+
public void custom_parsing_of_sequence_value_from_an_argument_with_one_token()
152+
{
153+
var argument = new Argument<IEnumerable<int>>(result => result.Tokens.Single().Value.Split(',').Select(int.Parse));
154+
155+
argument.Parse("1,2,3")
156+
.FindResultFor(argument)
157+
.GetValueOrDefault()
158+
.Should()
159+
.BeEquivalentTo(new[] { 1, 2, 3 });
160+
}
161+
162+
[Fact]
163+
public void custom_parsing_of_sequence_value_from_an_argument_with_multiple_tokens()
164+
{
165+
var argument = new Argument<IEnumerable<int>>(result =>
166+
{
167+
return result.Tokens.Select(t => int.Parse(t.Value)).ToArray();
168+
});
169+
170+
argument.Parse("1 2 3")
171+
.FindResultFor(argument)
172+
.GetValueOrDefault()
173+
.Should()
174+
.BeEquivalentTo(new[] { 1, 2, 3 });
175+
}
176+
177+
[Fact]
178+
public void custom_parsing_of_scalar_value_from_an_argument_with_multiple_tokens()
179+
{
180+
var argument = new Argument<int>(result => result.Tokens.Select(t => int.Parse(t.Value)).Sum())
181+
{
182+
Arity = ArgumentArity.ZeroOrMore
183+
};
184+
185+
argument.Parse("1 2 3")
186+
.FindResultFor(argument)
187+
.GetValueOrDefault()
188+
.Should()
189+
.Be(6);
190+
}
191+
}
192+
53193
protected override Symbol CreateSymbol(string name)
54194
{
55195
return new Argument(name);

src/System.CommandLine.Tests/Binding/TypeConversionTests.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class TypeConversionTests
1818
[Fact]
1919
public void Custom_types_and_conversion_logic_can_be_specified()
2020
{
21-
var argument = new Argument<MyCustomType>((SymbolResult parsed, out MyCustomType value) =>
21+
var argument = new Argument<MyCustomType>((ArgumentResult parsed, out MyCustomType value) =>
2222
{
2323
var custom = new MyCustomType();
2424
foreach (var a in parsed.Tokens)
@@ -261,14 +261,14 @@ public void When_argument_cannot_be_parsed_as_the_specified_type_then_getting_va
261261
{
262262
new Option(new[] { "-o", "--one" })
263263
{
264-
Argument = new Argument<int>((SymbolResult symbol, out int value) =>
264+
Argument = new Argument<int>((ArgumentResult argumentResult, out int value) =>
265265
{
266-
if (int.TryParse(symbol.Tokens.Select(t => t.Value).Single(), out value))
266+
if (int.TryParse(argumentResult.Tokens.Select(t => t.Value).Single(), out value))
267267
{
268268
return true;
269269
}
270270

271-
symbol.ErrorMessage = $"'{symbol.Token.Value}' is not an integer";
271+
argumentResult.ErrorMessage = $"'{argumentResult.Tokens.Single().Value}' is not an integer";
272272

273273
return false;
274274
}),
@@ -674,7 +674,7 @@ public void When_custom_converter_is_specified_and_an_argument_is_of_the_wrong_t
674674
{
675675
var command = new Command("tally")
676676
{
677-
new Argument<int>((SymbolResult symbolResult, out int value) =>
677+
new Argument<int>((ArgumentResult symbolResult, out int value) =>
678678
{
679679
value = default;
680680
symbolResult.ErrorMessage = "Could not parse int";
@@ -698,7 +698,7 @@ public void When_custom_conversion_fails_then_an_option_does_not_accept_further_
698698
new Argument<string>(),
699699
new Option("-x")
700700
{
701-
Argument = new Argument<string>((SymbolResult symbolResult, out string value) =>
701+
Argument = new Argument<string>((ArgumentResult symbolResult, out string value) =>
702702
{
703703
value = null;
704704
return false;
@@ -984,10 +984,10 @@ public async Task Custom_argument_converter_is_only_called_once()
984984
callCount.Should().Be(1);
985985
handlerWasCalled.Should().BeTrue();
986986

987-
bool TryConvertInt(SymbolResult result, out int value)
987+
bool TryConvertInt(ArgumentResult result, out int value)
988988
{
989989
callCount++;
990-
return int.TryParse(result.Token.Value, out value);
990+
return int.TryParse(result.Tokens.Single().Value, out value);
991991
}
992992

993993
void Run(int value) => handlerWasCalled = true;

src/System.CommandLine/Argument.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ internal TryConvertArgument ConvertArguments
6464
if (Arity.MaximumNumberOfValues == 1 &&
6565
ArgumentType == typeof(bool))
6666
{
67-
_convertArguments = (SymbolResult symbol, out object value) =>
67+
_convertArguments = (ArgumentResult symbol, out object value) =>
6868
{
6969
value = ArgumentConverter.ConvertObject(
7070
this,

src/System.CommandLine/ArgumentArity.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ internal static FailedArgumentConversionArityResult Validate(
5959
throw new ArgumentException("");
6060
}
6161

62-
6362
var tokenCount = argumentResult?.Tokens.Count ?? 0;
6463

6564
if (tokenCount < minimumNumberOfValues)
@@ -87,7 +86,7 @@ internal static FailedArgumentConversionArityResult Validate(
8786
return new TooManyArgumentsConversionResult(
8887
argument,
8988
symbolResult.ValidationMessages.ExpectsFewerArguments(
90-
symbolResult.Token,
89+
symbolResult.Tokens.Last(),
9190
tokenCount,
9291
maximumNumberOfValues));
9392
}

src/System.CommandLine/Argument{T}.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public Argument(TryConvertArgument<T> convert, Func<T> getDefaultValue = default
4545
throw new ArgumentNullException(nameof(convert));
4646
}
4747

48-
ConvertArguments = (SymbolResult result, out object value) =>
48+
ConvertArguments = (ArgumentResult result, out object value) =>
4949
{
5050
if (convert(result, out var valueObj))
5151
{
@@ -64,5 +64,36 @@ public Argument(TryConvertArgument<T> convert, Func<T> getDefaultValue = default
6464
SetDefaultValueFactory(() => getDefaultValue());
6565
}
6666
}
67+
68+
public Argument(ParseArgument<T> parse, bool isDefault = false) : this()
69+
{
70+
if (isDefault)
71+
{
72+
SetDefaultValueFactory(() =>
73+
{
74+
var argumentResult = new ArgumentResult(
75+
this,
76+
null);
77+
78+
return parse(argumentResult);
79+
});
80+
}
81+
82+
ConvertArguments = (ArgumentResult argumentResult, out object value) =>
83+
{
84+
var result = parse(argumentResult);
85+
86+
if (string.IsNullOrEmpty(argumentResult.ErrorMessage))
87+
{
88+
value = result;
89+
return true;
90+
}
91+
else
92+
{
93+
value = default(T);
94+
return false;
95+
}
96+
};
97+
}
6798
}
6899
}

src/System.CommandLine/Binding/TryConvertArgument.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,19 @@
55

66
namespace System.CommandLine.Binding
77
{
8-
public delegate bool TryConvertArgument(SymbolResult symbolResult, out object value);
8+
internal delegate bool TryConvertArgument(
9+
ArgumentResult argumentResult,
10+
out object value);
911

10-
public delegate bool TryConvertArgument<T>(SymbolResult symbolResult, out T value);
11-
}
12+
/// <summary>
13+
/// Converts an <see cref="ArgumentResult"/> into an instance of <typeparamref name="T"/>.
14+
/// </summary>
15+
/// <typeparam name="T">The type to convert to.</typeparam>
16+
/// <param name="argumentResult">The <see cref="ArgumentResult"/> representing parsed input to be converted.</param>
17+
/// <param name="value">The converted result.</param>
18+
/// <returns>True if the conversion is successful; otherwise, false.</returns>
19+
/// <remarks>Validation errors can be returned by setting <see cref="SymbolResult.ErrorMessage"/>.</remarks>
20+
public delegate bool TryConvertArgument<T>(
21+
ArgumentResult argumentResult,
22+
out T value);
23+
}

src/System.CommandLine/Parsing/ArgumentResult.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ public class ArgumentResult : SymbolResult
1010
{
1111
internal ArgumentResult(
1212
IArgument argument,
13-
Token token,
14-
SymbolResult parent) : base(argument, token, parent)
13+
SymbolResult parent) : base(argument, parent)
1514
{
1615
Argument = argument;
1716
}
@@ -20,8 +19,8 @@ internal ArgumentResult(
2019

2120
public IArgument Argument { get; }
2221

23-
internal override ArgumentConversionResult ArgumentConversionResult =>
24-
ConversionResult ??= Convert(this, Argument);
22+
internal ArgumentConversionResult ArgumentConversionResult =>
23+
ConversionResult ??= Convert(Argument);
2524

2625
public override string ToString() => $"{GetType().Name} {Argument.Name}: {string.Join(" ", Tokens.Select(t => $"<{t.Value}>"))}";
2726

@@ -40,11 +39,10 @@ internal ParseError CustomError(Argument argument)
4039
return null;
4140
}
4241

43-
internal static ArgumentConversionResult Convert(
44-
ArgumentResult argumentResult,
42+
internal virtual ArgumentConversionResult Convert(
4543
IArgument argument)
4644
{
47-
var parentResult = argumentResult.Parent;
45+
var parentResult = Parent;
4846

4947
if (ShouldCheckArity() &&
5048
ArgumentArity.Validate(parentResult,
@@ -65,12 +63,12 @@ internal static ArgumentConversionResult Convert(
6563
if (argument is Argument a &&
6664
a.ConvertArguments != null)
6765
{
68-
if (argumentResult.ConversionResult != null)
66+
if (ConversionResult != null)
6967
{
70-
return argumentResult.ConversionResult;
68+
return ConversionResult;
7169
}
7270

73-
var success = a.ConvertArguments(argumentResult, out var value);
71+
var success = a.ConvertArguments(this, out var value);
7472

7573
if (value is ArgumentConversionResult conversionResult)
7674
{
@@ -82,7 +80,7 @@ internal static ArgumentConversionResult Convert(
8280
}
8381
else
8482
{
85-
return ArgumentConversionResult.Failure(argument, argumentResult.ErrorMessage ?? $"Invalid: {parentResult.Token} {string.Join(" ", parentResult.Tokens.Select(t => t.Value))}");
83+
return ArgumentConversionResult.Failure(argument, ErrorMessage ?? $"Invalid: {parentResult.Token()} {string.Join(" ", parentResult.Tokens.Select(t => t.Value))}");
8684
}
8785
}
8886

@@ -104,6 +102,5 @@ bool ShouldCheckArity()
104102
optionResult.IsImplicit);
105103
}
106104
}
107-
108105
}
109106
}

0 commit comments

Comments
 (0)