Skip to content

Commit 434a14f

Browse files
Merge pull request #2413 from KathleenDollard/value-subsystem
Value subsystem and core changes
2 parents da0b357 + 6094c81 commit 434a14f

25 files changed

+934
-270
lines changed

src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
-->
3333
<Compile Include="AlternateSubsystems.cs" />
3434
<Compile Include="Constants.cs" />
35+
<Compile Include="ValueSubsystemTests.cs" />
3536
<Compile Include="ResponseSubsystemTests.cs" />
3637
<Compile Include="DirectiveSubsystemTests.cs" />
3738
<Compile Include="DiagramSubsystemTests.cs" />

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,18 @@ internal class Directive : IEnumerable<object[]>
7979

8080
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
8181
}
82+
83+
internal class Value : IEnumerable<object[]>
84+
{
85+
private readonly List<object[]> _data =
86+
[
87+
["--intValue", 42],
88+
["--stringValue", "43"],
89+
["--boolValue", true]
90+
];
91+
92+
public IEnumerator<object[]> GetEnumerator() => _data.GetEnumerator();
93+
94+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
95+
}
8296
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using FluentAssertions;
5+
using System.CommandLine.Directives;
6+
using System.CommandLine.Parsing;
7+
using Xunit;
8+
9+
namespace System.CommandLine.Subsystems.Tests;
10+
11+
public class ValueSubsystemTests
12+
{
13+
[Fact]
14+
public void ValueSubsystem_is_activated_by_default()
15+
{
16+
CliRootCommand rootCommand = [
17+
new CliCommand("x")
18+
{
19+
new CliOption<string>("--opt1")
20+
}];
21+
var configuration = new CliConfiguration(rootCommand);
22+
var subsystem = new ValueSubsystem();
23+
var input = "x --opt1 Kirk";
24+
var args = CliParser.SplitCommandLine(input).ToList();
25+
26+
Subsystem.Initialize(subsystem, configuration, args);
27+
var parseResult = CliParser.Parse(rootCommand, args[0], configuration);
28+
var isActive = Subsystem.GetIsActivated(subsystem, parseResult);
29+
30+
isActive.Should().BeTrue();
31+
}
32+
33+
[Fact]
34+
public void ValueSubsystem_returns_values_that_are_entered()
35+
{
36+
var consoleHack = new ConsoleHack().RedirectToBuffer(true);
37+
CliOption<int> option = new CliOption<int>("--intValue");
38+
CliRootCommand rootCommand = [
39+
new CliCommand("x")
40+
{
41+
option
42+
}];
43+
var configuration = new CliConfiguration(rootCommand);
44+
var pipeline = Pipeline.CreateEmpty();
45+
pipeline.Value = new ValueSubsystem();
46+
const int expected = 42;
47+
var input = $"x --intValue {expected}";
48+
49+
var parseResult = pipeline.Parse(configuration, input); // assigned for debugging
50+
pipeline.Execute(configuration, input, consoleHack);
51+
52+
pipeline.Value.GetValue<int>(option).Should().Be(expected);
53+
}
54+
55+
[Fact]
56+
public void ValueSubsystem_returns_default_value_when_no_value_is_entered()
57+
{
58+
var consoleHack = new ConsoleHack().RedirectToBuffer(true);
59+
CliOption<int> option = new CliOption<int>("--intValue");
60+
CliRootCommand rootCommand = [option];
61+
var configuration = new CliConfiguration(rootCommand);
62+
var pipeline = Pipeline.CreateEmpty();
63+
pipeline.Value = new ValueSubsystem();
64+
pipeline.Value.DefaultValue.Set(option, 43);
65+
const int expected = 43;
66+
var input = $"";
67+
68+
pipeline.Execute(configuration, input, consoleHack);
69+
70+
pipeline.Value.GetValue<int>(option).Should().Be(expected);
71+
}
72+
73+
74+
[Fact]
75+
public void ValueSubsystem_returns_calculated_default_value_when_no_value_is_entered()
76+
{
77+
var consoleHack = new ConsoleHack().RedirectToBuffer(true);
78+
CliOption<int> option = new CliOption<int>("--intValue");
79+
CliRootCommand rootCommand = [option];
80+
var configuration = new CliConfiguration(rootCommand);
81+
var pipeline = Pipeline.CreateEmpty();
82+
pipeline.Value = new ValueSubsystem();
83+
var x = 42;
84+
pipeline.Value.DefaultValueCalculation.Set(option, () => x + 2);
85+
const int expected = 44;
86+
var input = "";
87+
88+
var parseResult = pipeline.Parse(configuration, input); // assigned for debugging
89+
pipeline.Execute(configuration, input, consoleHack);
90+
91+
pipeline.Value.GetValue<int>(option).Should().Be(expected);
92+
}
93+
}

src/System.CommandLine.Subsystems/Directives/DiagramSubsystem.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ internal static StringBuilder Diagram(ParseResult parseResult)
5757
return builder;
5858
}
5959

60+
/*
6061
private static void Diagram(
6162
StringBuilder builder,
6263
SymbolResult symbolResult,
@@ -66,7 +67,7 @@ private static void Diagram(
6667
{
6768
builder.Append('!');
6869
}
69-
70+
*/
7071
// TODO: Directives
7172
/*
7273
switch (symbolResult)
@@ -176,6 +177,6 @@ private static void Diagram(
176177
}
177178
}
178179
}
179-
*/
180180
}
181+
*/
181182
}

src/System.CommandLine.Subsystems/Pipeline.cs

Lines changed: 30 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ namespace System.CommandLine;
99

1010
public class Pipeline
1111
{
12+
//TODO: When we allow adding subsystems, this code will change
13+
private IEnumerable<CliSubsystem?> Subsystems
14+
=> [Help, Version, Completion, Diagram, Value, ErrorReporting];
15+
1216
public static Pipeline Create(HelpSubsystem? help = null,
1317
VersionSubsystem? version = null,
1418
CompletionSubsystem? completion = null,
@@ -25,7 +29,7 @@ public static Pipeline Create(HelpSubsystem? help = null,
2529
Value = value ?? new ValueSubsystem()
2630
};
2731

28-
public static Pipeline CreateEmpty()
32+
public static Pipeline CreateEmpty()
2933
=> new();
3034

3135
private Pipeline() { }
@@ -63,61 +67,6 @@ public CliExit Execute(ParseResult parseResult, string rawInput, ConsoleHack? co
6367
return new CliExit(pipelineContext);
6468
}
6569

66-
protected virtual void InitializeHelp(InitializationContext context)
67-
=> Help?.Initialize(context);
68-
69-
protected virtual void InitializeVersion(InitializationContext context)
70-
=> Version?.Initialize(context);
71-
72-
protected virtual void InitializeCompletion(InitializationContext context)
73-
=> Completion?.Initialize(context);
74-
75-
protected virtual void InitializeDiagram(InitializationContext context)
76-
=> Diagram?.Initialize(context);
77-
78-
protected virtual void InitializeErrorReporting(InitializationContext context)
79-
=> ErrorReporting?.Initialize(context);
80-
81-
protected virtual CliExit TearDownHelp(CliExit cliExit)
82-
=> Help is null
83-
? cliExit
84-
: Help.TearDown(cliExit);
85-
86-
protected virtual CliExit? TearDownVersion(CliExit cliExit)
87-
=> Version is null
88-
? cliExit
89-
: Version.TearDown(cliExit);
90-
91-
protected virtual CliExit TearDownCompletion(CliExit cliExit)
92-
=> Completion is null
93-
? cliExit
94-
: Completion.TearDown(cliExit);
95-
96-
protected virtual CliExit TearDownDiagram(CliExit cliExit)
97-
=> Diagram is null
98-
? cliExit
99-
: Diagram.TearDown(cliExit);
100-
101-
protected virtual CliExit TearDownErrorReporting(CliExit cliExit)
102-
=> ErrorReporting is null
103-
? cliExit
104-
: ErrorReporting.TearDown(cliExit);
105-
106-
protected virtual void ExecuteHelp(PipelineContext context)
107-
=> ExecuteIfNeeded(Help, context);
108-
109-
protected virtual void ExecuteVersion(PipelineContext context)
110-
=> ExecuteIfNeeded(Version, context);
111-
112-
protected virtual void ExecuteCompletion(PipelineContext context)
113-
=> ExecuteIfNeeded(Completion, context);
114-
115-
protected virtual void ExecuteDiagram(PipelineContext context)
116-
=> ExecuteIfNeeded(Diagram, context);
117-
118-
protected virtual void ExecuteErrorReporting(PipelineContext context)
119-
=> ExecuteIfNeeded(ErrorReporting, context);
120-
12170
// TODO: Consider whether this should be public. It would simplify testing, but would it do anything else
12271
// TODO: Confirm that it is OK for ConsoleHack to be unavailable in Initialize
12372
/// <summary>
@@ -131,11 +80,13 @@ protected virtual void ExecuteErrorReporting(PipelineContext context)
13180
/// </remarks>
13281
protected virtual void InitializeSubsystems(InitializationContext context)
13382
{
134-
InitializeHelp(context);
135-
InitializeVersion(context);
136-
InitializeCompletion(context);
137-
InitializeDiagram(context);
138-
InitializeErrorReporting(context);
83+
foreach (var subsystem in Subsystems)
84+
{
85+
if (subsystem is not null)
86+
{
87+
subsystem.Initialize(context);
88+
}
89+
}
13990
}
14091

14192
// TODO: Consider whether this should be public
@@ -144,26 +95,31 @@ protected virtual void InitializeSubsystems(InitializationContext context)
14495
/// Perform any cleanup operations
14596
/// </summary>
14697
/// <param name="pipelineContext">The context of the current execution</param>
147-
/// <remarks>
148-
/// Note to inheritors: The ordering of tear down should normally be in the reverse order than initializing
149-
/// </remarks>
15098
protected virtual CliExit TearDownSubsystems(CliExit cliExit)
15199
{
152-
TearDownErrorReporting(cliExit);
153-
TearDownDiagram(cliExit);
154-
TearDownCompletion(cliExit);
155-
TearDownVersion(cliExit);
156-
TearDownHelp(cliExit);
100+
// TODO: Work on this design as the last cliExit wins and they may not all be well behaved
101+
var subsystems = Subsystems.Reverse();
102+
foreach (var subsystem in subsystems)
103+
{
104+
if (subsystem is not null)
105+
{
106+
cliExit = subsystem.TearDown(cliExit);
107+
}
108+
}
157109
return cliExit;
158110
}
159111

160112
protected virtual void ExecuteSubsystems(PipelineContext pipelineContext)
161113
{
162-
ExecuteHelp(pipelineContext);
163-
ExecuteVersion(pipelineContext);
164-
ExecuteCompletion(pipelineContext);
165-
ExecuteDiagram(pipelineContext);
166-
ExecuteErrorReporting(pipelineContext);
114+
// TODO: Consider redesign where pipelineContext is not modifiable.
115+
//
116+
foreach (var subsystem in Subsystems)
117+
{
118+
if (subsystem is not null)
119+
{
120+
pipelineContext = subsystem.ExecuteIfNeeded(pipelineContext);
121+
}
122+
}
167123
}
168124

169125
protected static void ExecuteIfNeeded(CliSubsystem? subsystem, PipelineContext pipelineContext)

src/System.CommandLine.Subsystems/Subsystems/Annotations/AnnotationAccessor.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ namespace System.CommandLine.Subsystems.Annotations;
88
/// <summary>
99
/// Allows associating an annotation with a <see cref="CliSymbol"/>. The annotation will be stored by the accessor's owner <see cref="CliSubsystem"/>.
1010
/// </summary>
11-
/// <remarks>
12-
/// The annotation will be stored by the accessor's owner <see cref="CliSubsystem"/>.
13-
/// </summary>
14-
/// <typeparam name="TValue">The type of value to be stored</typeparam>
15-
/// <param name="owner">The subsystem that this annotation store data for.</param>
16-
/// <param name="id">The identifier for this annotation, since subsystems may have multiple annotations.</param>
1711
public struct AnnotationAccessor<TValue>(CliSubsystem owner, AnnotationId<TValue> id)
1812
{
1913
/// <summary>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace System.CommandLine.Subsystems.Annotations;
7+
8+
/// <summary>
9+
/// Associates an annotation with a <see cref="CliSymbol"/>. The symbol must be an option or argument and the value must be of the same type as the symbol./>.
10+
/// </summary>
11+
/// <remarks>
12+
/// The annotation will be stored by the accessor's owner <see cref="CliSubsystem"/>.
13+
/// </remarks>
14+
/// <typeparam name="TValue">The type of value to be stored</typeparam>
15+
/// <param name="owner">The subsystem that this annotation store data for.</param>
16+
/// <param name="id">The identifier for this annotation, since subsystems may have multiple annotations.</param>
17+
public struct ValueAnnotationAccessor<TValue>(CliSubsystem owner, AnnotationId<TValue> id)
18+
{
19+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Id"/>>
20+
public AnnotationId<TValue> Id { get; }
21+
22+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Set"/>>
23+
public readonly void Set<TSymbolValue>(CliOption<TSymbolValue> symbol, TSymbolValue value)
24+
where TSymbolValue : TValue
25+
=> owner.SetAnnotation(symbol, id, value);
26+
27+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Set"/>>
28+
public readonly void Set<TSymbolValue>(CliArgument<TSymbolValue> symbol, TSymbolValue value)
29+
where TSymbolValue : TValue
30+
=> owner.SetAnnotation(symbol, id, value);
31+
32+
// TODO: Consider whether we need a version that takes a CliSymbol (ValueSymbol)
33+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Get"/>>
34+
public readonly bool TryGet<TSymbolValue>(CliOption<TSymbolValue> symbol, [NotNullWhen(true)] out TValue? value)
35+
where TSymbolValue : TValue
36+
=> TryGetInternal<TSymbolValue>(symbol, out value);
37+
38+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Get"/>>
39+
public readonly bool TryGet<TSymbolValue>(CliArgument<TSymbolValue> symbol, [NotNullWhen(true)] out TValue? value)
40+
where TSymbolValue : TValue
41+
=> TryGetInternal<TSymbolValue>(symbol, out value);
42+
43+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Get"/>>
44+
/// <remarks>
45+
/// This overload will throw if the stored value cannot be converted to the type.
46+
/// </remarks>
47+
/// <exception cref="InvalidCastException"/>
48+
public readonly bool TryGet<TSymbolValue>(CliSymbol symbol, [NotNullWhen(true)] out TValue? value)
49+
where TSymbolValue : TValue
50+
=> TryGetInternal<TSymbolValue>(symbol, out value);
51+
52+
private readonly bool TryGetInternal<TSymbolValue>(CliSymbol symbol, [NotNullWhen(true)] out TValue? value)
53+
where TSymbolValue : TValue
54+
{
55+
if (owner.TryGetAnnotation(symbol, id, out var storedValue))
56+
{
57+
if (storedValue is TSymbolValue symbolValue)
58+
{
59+
value = symbolValue;
60+
return true;
61+
}
62+
throw new ArgumentException("The requested type is incorrect.", nameof(symbol));
63+
}
64+
value = default;
65+
return false;
66+
}
67+
}

0 commit comments

Comments
 (0)