Skip to content

Commit 5ba9513

Browse files
Incorporated RangeBound and ValueSource
1 parent ce67d0f commit 5ba9513

File tree

5 files changed

+236
-26
lines changed

5 files changed

+236
-26
lines changed

src/System.CommandLine.Subsystems.Tests/ValidationSubsystemTests.cs

Lines changed: 175 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using FluentAssertions;
5+
using Microsoft.VisualBasic.FileIO;
56
using System.CommandLine.Directives;
67
using System.CommandLine.Parsing;
8+
using System.CommandLine.ValueConditions;
79
using Xunit;
810
using static System.CommandLine.Subsystems.Tests.TestData;
911

@@ -12,39 +14,67 @@ namespace System.CommandLine.Subsystems.Tests;
1214
public class ValidationSubsystemTests
1315
{
1416
// Running exactly the same code is important here because missing a step will result in a false positive. Ask me how I know
15-
private (CliCommand rootCommand, CliConfiguration configuration) GetCliWithRange<T>(T lowerBound, T upperBound)
16-
where T: IComparable<T>
17+
private CliOption GetOptionWithSimpleRange<T>(T lowerBound, T upperBound)
18+
where T : IComparable<T>
1719
{
1820
var option = new CliOption<int>("--intOpt");
1921
option.SetRange(lowerBound, upperBound);
20-
var rootCommand = new CliRootCommand { option };
21-
return (rootCommand, new CliConfiguration(rootCommand));
22+
return option;
2223
}
2324

24-
private PipelineResult ExecutedPipelineResultForRange<T>(T lowerBound, T upperBound, string input)
25+
private CliOption GetOptionWithRangeBounds<T>(RangeBound<T> lowerBound, RangeBound<T> upperBound)
2526
where T : IComparable<T>
2627
{
27-
(var rootCommand, var configuration) = GetCliWithRange(lowerBound, upperBound);
28+
var option = new CliOption<int>("--intOpt");
29+
option.SetRange(lowerBound, upperBound);
30+
return option;
31+
}
32+
33+
private PipelineResult ExecutedPipelineResultForRangeOption(CliOption option, string input)
34+
{
35+
var command = new CliRootCommand { option };
36+
return ExecutedPipelineResultForCommand(command, input);
37+
}
38+
39+
private PipelineResult ExecutedPipelineResultForCommand(CliCommand command, string input)
40+
{
2841
var validationSubsystem = ValidationSubsystem.Create();
29-
var parseResult = CliParser.Parse(rootCommand, input, configuration);
30-
var pipelineResult = new PipelineResult(parseResult, input, null);
42+
var parseResult = CliParser.Parse(command, input, new CliConfiguration(command));
43+
var pipelineResult = new PipelineResult(parseResult, input, Pipeline.CreateEmpty());
3144
validationSubsystem.Execute(pipelineResult);
3245
return pipelineResult;
3346
}
3447

3548
[Fact]
3649
public void Int_values_in_specified_range_do_not_have_errors()
3750
{
38-
var pipelineResult = ExecutedPipelineResultForRange(0, 50,"--intOpt 42");
51+
var option = GetOptionWithSimpleRange(0, 50);
52+
53+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
3954

4055
pipelineResult.Should().NotBeNull();
4156
pipelineResult.GetErrors().Should().BeEmpty();
4257
}
4358

4459
[Fact]
45-
public void Int_values_not_in_specified_range_report_error()
60+
public void Int_values_above_upper_bound_report_error()
61+
{
62+
var option = GetOptionWithSimpleRange(0, 5);
63+
64+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
65+
66+
pipelineResult.Should().NotBeNull();
67+
pipelineResult.GetErrors().Should().HaveCount(1);
68+
var error = pipelineResult.GetErrors().First();
69+
// TODO: Create test mechanism for CliDiagnostics
70+
}
71+
72+
[Fact]
73+
public void Int_below_lower_bound_report_error()
4674
{
47-
var pipelineResult = ExecutedPipelineResultForRange(0, 5, "--intOpt 42");
75+
var option = GetOptionWithSimpleRange(0, 5);
76+
77+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt -42");
4878

4979
pipelineResult.Should().NotBeNull();
5080
pipelineResult.GetErrors().Should().HaveCount(1);
@@ -55,7 +85,9 @@ public void Int_values_not_in_specified_range_report_error()
5585
[Fact]
5686
public void Int_values_on_lower_range_bound_do_not_report_error()
5787
{
58-
var pipelineResult = ExecutedPipelineResultForRange(42, 50, "--intOpt 42");
88+
var option = GetOptionWithSimpleRange(42, 50);
89+
90+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
5991

6092
pipelineResult.Should().NotBeNull();
6193
pipelineResult.GetErrors().Should().BeEmpty();
@@ -64,11 +96,141 @@ public void Int_values_on_lower_range_bound_do_not_report_error()
6496
[Fact]
6597
public void Int_values_on_upper_range_bound_do_not_report_error()
6698
{
67-
var pipelineResult = ExecutedPipelineResultForRange(0, 42, "--intOpt 42");
99+
var option = GetOptionWithSimpleRange(0, 42);
100+
101+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
68102

69103
pipelineResult.Should().NotBeNull();
70104
pipelineResult.GetErrors().Should().BeEmpty();
71105
}
72106

107+
[Fact]
108+
public void Values_below_calculated_lower_bound_report_error()
109+
{
110+
var option = GetOptionWithRangeBounds(RangeBound<int>.Create(() => 1), 50);
111+
112+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 0");
113+
114+
pipelineResult.Should().NotBeNull();
115+
pipelineResult.GetErrors().Should().HaveCount(1);
116+
var error = pipelineResult.GetErrors().First();
117+
// TODO: Create test mechanism for CliDiagnostics
118+
}
119+
120+
121+
[Fact]
122+
public void Values_within_calculated_range_do_not_report_error()
123+
{
124+
var option = GetOptionWithRangeBounds(RangeBound<int>.Create(() => 1), RangeBound<int>.Create(() => 50));
125+
126+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
127+
128+
pipelineResult.Should().NotBeNull();
129+
pipelineResult.GetErrors().Should().BeEmpty();
130+
}
131+
132+
[Fact]
133+
public void Values_above_calculated_upper_bound_report_error()
134+
{
135+
var option = GetOptionWithRangeBounds(0,RangeBound<int>.Create(() => 40));
136+
137+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
138+
139+
pipelineResult.Should().NotBeNull();
140+
pipelineResult.GetErrors().Should().HaveCount(1);
141+
var error = pipelineResult.GetErrors().First();
142+
// TODO: Create test mechanism for CliDiagnostics
143+
}
144+
145+
[Fact]
146+
public void Values_below_relative_lower_bound_report_error()
147+
{
148+
var otherOption = new CliOption<int>("-a");
149+
var option = GetOptionWithRangeBounds(RangeBound<int>.Create(otherOption, o => (int)o + 1), 50);
150+
var command = new CliCommand("cmd") { option, otherOption };
151+
152+
var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0");
153+
154+
pipelineResult.Should().NotBeNull();
155+
pipelineResult.GetErrors().Should().HaveCount(1);
156+
var error = pipelineResult.GetErrors().First();
157+
// TODO: Create test mechanism for CliDiagnostics
158+
}
159+
160+
161+
[Fact]
162+
public void Values_within_relative_range_do_not_report_error()
163+
{
164+
var otherOption = new CliOption<int>("-a");
165+
var option = GetOptionWithRangeBounds(RangeBound<int>.Create(otherOption, o => (int)o + 1), RangeBound<int>.Create(otherOption, o => (int)o + 10));
166+
var command = new CliCommand("cmd") { option, otherOption };
167+
168+
var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 11 -a 3");
169+
170+
pipelineResult.Should().NotBeNull();
171+
pipelineResult.GetErrors().Should().BeEmpty();
172+
}
173+
174+
[Fact]
175+
public void Values_above_relative_upper_bound_report_error()
176+
{
177+
var otherOption = new CliOption<int>("-a");
178+
var option = GetOptionWithRangeBounds(0, RangeBound<int>.Create(otherOption, o => (int)o + 10));
179+
var command = new CliCommand("cmd") { option, otherOption };
180+
181+
var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 9 -a -2");
182+
183+
pipelineResult.Should().NotBeNull();
184+
pipelineResult.GetErrors().Should().HaveCount(1);
185+
var error = pipelineResult.GetErrors().First();
186+
// TODO: Create test mechanism for CliDiagnostics
187+
}
188+
189+
[Fact]
190+
public void Values_below_environment_lower_bound_report_error()
191+
{
192+
var envName = "SYSTEM_COMMANDLINE_LOWERBOUND";
193+
Environment.SetEnvironmentVariable(envName, "2");
194+
var option = GetOptionWithRangeBounds(RangeBound<int>.Create(envName, s => int.Parse(s) + 1), 50);
195+
196+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 2");
197+
Environment.SetEnvironmentVariable(envName, null);
198+
199+
pipelineResult.Should().NotBeNull();
200+
pipelineResult.GetErrors().Should().HaveCount(1);
201+
var error = pipelineResult.GetErrors().First();
202+
// TODO: Create test mechanism for CliDiagnostics
203+
}
204+
205+
206+
[Fact]
207+
public void Values_within_environment_range_do_not_report_error()
208+
{
209+
var envName = "SYSTEM_COMMANDLINE_LOWERBOUND";
210+
Environment.SetEnvironmentVariable(envName, "2");
211+
var option = GetOptionWithRangeBounds(RangeBound<int>.Create(envName, s => int.Parse(s) + 1), 50);
212+
213+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 11");
214+
Environment.SetEnvironmentVariable(envName, null);
215+
216+
pipelineResult.Should().NotBeNull();
217+
pipelineResult.GetErrors().Should().BeEmpty();
218+
}
219+
220+
[Fact]
221+
public void Values_above_environment_upper_bound_report_error()
222+
{
223+
var envName = "SYSTEM_COMMANDLINE_LOWERBOUND";
224+
Environment.SetEnvironmentVariable(envName, "2");
225+
var option = GetOptionWithRangeBounds(0,RangeBound<int>.Create(envName, s => int.Parse(s) + 1));
226+
227+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 4");
228+
Environment.SetEnvironmentVariable(envName, null);
229+
230+
pipelineResult.Should().NotBeNull();
231+
pipelineResult.GetErrors().Should().HaveCount(1);
232+
var error = pipelineResult.GetErrors().First();
233+
// TODO: Create test mechanism for CliDiagnostics
234+
}
73235

74236
}

src/System.CommandLine.Subsystems/ValueConditionAnnotationExtensions.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,30 @@ public static void SetRange<T>(this CliValueSymbol symbol, T lowerBound, T upper
1414
symbol.SetValueCondition(range);
1515
}
1616

17+
public static void SetRange<T>(this CliValueSymbol symbol, RangeBound<T> lowerBound, T upperBound)
18+
where T : IComparable<T>
19+
{
20+
var range = new Range<T>(lowerBound, upperBound);
21+
22+
symbol.SetValueCondition(range);
23+
}
24+
25+
public static void SetRange<T>(this CliValueSymbol symbol, T lowerBound, RangeBound<T> upperBound)
26+
where T : IComparable<T>
27+
{
28+
var range = new Range<T>(lowerBound, upperBound);
29+
30+
symbol.SetValueCondition(range);
31+
}
32+
33+
public static void SetRange<T>(this CliValueSymbol symbol, RangeBound<T> lowerBound, RangeBound<T> upperBound)
34+
where T : IComparable<T>
35+
{
36+
var range = new Range<T>(lowerBound, upperBound);
37+
38+
symbol.SetValueCondition(range);
39+
}
40+
1741
public static void SetInclusiveGroup(this CliCommand symbol, IEnumerable<CliValueSymbol> group)
1842
=> symbol.SetValueCondition(new InclusiveGroup(group));
1943

@@ -51,7 +75,7 @@ public static void SetValueCondition<TValueCondition>(this CliCommand symbol, TV
5175
: null;
5276

5377
public static TCondition? GetValueCondition<TCondition>(this CliValueSymbol symbol)
54-
where TCondition : ValueCondition
78+
where TCondition : ValueCondition
5579
=> !symbol.TryGetAnnotation(ValueConditionAnnotations.ValueConditions, out List<ValueCondition>? valueConditions)
5680
? null
5781
: valueConditions.OfType<TCondition>().LastOrDefault();

src/System.CommandLine.Subsystems/ValueConditions/Range.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ protected Range(Type valueType)
1616
public Type ValueType { get; }
1717
}
1818

19-
public class Range<T>(T? lowerBound, T? upperBound)
19+
public class Range<T>(RangeBound<T>? lowerBound, RangeBound<T>? upperBound)
2020
: Range(typeof(T)), IValueValidator
2121
where T : IComparable<T>
2222
{
@@ -34,21 +34,23 @@ public void Validate(object? value,
3434
// TODO: Replace the strings we are comparing with a diagnostic ID when we update ParseError
3535
if (LowerBound is not null)
3636
{
37-
if (comparableValue.CompareTo(LowerBound) < 0)
37+
var lowerValue = LowerBound.ValueSource.GetTypedValue(validationContext.PipelineResult);
38+
if (comparableValue.CompareTo(lowerValue) < 0)
3839
{
3940
validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is below the lower bound of {LowerBound}"));
4041
}
4142
}
4243

4344
if (UpperBound is not null)
4445
{
45-
if (comparableValue.CompareTo(UpperBound) > 0)
46+
var upperValue = UpperBound.ValueSource.GetTypedValue(validationContext.PipelineResult);
47+
if (comparableValue.CompareTo(upperValue) > 0)
4648
{
4749
validationContext.PipelineResult.AddError(new ParseError($"The value for '{valueSymbol.Name}' is above the upper bound of {UpperBound}"));
4850
}
4951
}
5052
}
5153

52-
public T? LowerBound { get; init; } = lowerBound;
53-
public T? UpperBound { get; init; } = upperBound;
54+
public RangeBound<T>? LowerBound { get; init; } = lowerBound;
55+
public RangeBound<T>? UpperBound { get; init; } = upperBound;
5456
}

src/System.CommandLine.Subsystems/ValueConditions/RangeBound.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,24 @@
33

44
namespace System.CommandLine.ValueConditions;
55

6-
public class RangeBound
6+
public class RangeBound<T>(ValueSource<T> valueSource, bool exclusive = false)
77
{
8-
public ValueSource ValueSource { get; }
9-
public bool Exclusive { get; }
8+
9+
public static implicit operator RangeBound<T>(T value) => RangeBound<T>.Create(value);
10+
public static implicit operator RangeBound<T>(Func<T> calculated) => RangeBound<T>.Create(calculated);
11+
12+
public static RangeBound<T> Create(T value, string? description = null)
13+
=> new(new SimpleValueSource<T>(value, description));
14+
15+
public static RangeBound<T> Create(Func<T> calculation, string? description = null)
16+
=> new(new CalculatedValueSource<T>(calculation));
17+
18+
public static RangeBound<T> Create(CliValueSymbol otherSymbol, Func<object, T> calculation, string? description = null)
19+
=> new(new RelativeToSymbolValueSource<T>(otherSymbol, calculation, description));
20+
21+
public static RangeBound<T> Create(string environmentVariableName, Func<string, T> calculation, string? description = null)
22+
=> new(new RelativeToEnvironmentVariableValueSource<T>(environmentVariableName, calculation, description));
23+
24+
public ValueSource<T> ValueSource { get; } = valueSource;
25+
public bool Exclusive { get; } = exclusive;
1026
}

src/System.CommandLine.Subsystems/ValueConditions/ValueSource.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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 static System.Runtime.InteropServices.JavaScript.JSType;
5+
46
namespace System.CommandLine.ValueConditions;
57

68
public abstract class ValueSource
@@ -19,9 +21,13 @@ public abstract class ValueSource<T> : ValueSource
1921
{
2022
return GetTypedValue(pipelineResult);
2123
}
24+
25+
public static implicit operator ValueSource<T>(T value) => new SimpleValueSource<T>(value);
26+
public static implicit operator ValueSource<T>(Func<T> calculated) => new CalculatedValueSource<T>(calculated);
27+
2228
}
2329

24-
public class SimpleValueSource<T>(T value, string description)
30+
public class SimpleValueSource<T>(T value, string? description = null)
2531
: ValueSource<T>
2632
{
2733
public override string Description { get; } = description;
@@ -31,7 +37,7 @@ public override T GetTypedValue(PipelineResult pipelineResult)
3137
}
3238

3339
// Find an example of when this is useful beyond Random and Guid. Is a time lag between building the CLI and validating important (DateTime.Now())
34-
public class CalculatedValueSource<T>(Func<T> calculation, string description)
40+
public class CalculatedValueSource<T>(Func<T> calculation, string? description = null)
3541
: ValueSource<T>
3642
{
3743
public override string Description { get; } = description;
@@ -40,7 +46,7 @@ public override T GetTypedValue(PipelineResult pipelineResult)
4046
=> calculation();
4147
}
4248

43-
public class RelativeToSymbolValueSource<T>(CliValueSymbol otherSymbol, Func<object?, T> calculation, string description)
49+
public class RelativeToSymbolValueSource<T>(CliValueSymbol otherSymbol, Func<object, T> calculation, string? description)
4450
: ValueSource<T>
4551
{
4652
public override string Description { get; } = description;
@@ -49,7 +55,7 @@ public override T GetTypedValue(PipelineResult pipelineResult)
4955
=> calculation(pipelineResult.GetValue(otherSymbol));
5056
}
5157

52-
public class RelativeToEnvironmentVariableValueSource<T>(string environmentVariableName, Func<object?, T> calculation, string description)
58+
public class RelativeToEnvironmentVariableValueSource<T>(string environmentVariableName, Func<string, T> calculation, string? description)
5359
: ValueSource<T>
5460
{
5561
public override string Description { get; } = description;

0 commit comments

Comments
 (0)