Skip to content

Commit 262b6fc

Browse files
Use generic annotation accessors
Added Func<T> generic annotation accesssor Moved value annotation accessor to own file Removed Try<specific thing> from ValueSubsystem to encourage <annotationId>.<specific thing>.Get/Set Cleanup, took some analyzer suggestions XML Docs improved
1 parent 35f7ff1 commit 262b6fc

File tree

6 files changed

+201
-70
lines changed

6 files changed

+201
-70
lines changed

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

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,3 @@ public struct AnnotationAccessor<TValue>(CliSubsystem owner, AnnotationId<TValue
3636
/// <returns>True if the value was found, false otherwise.</returns>
3737
public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, Id, out value);
3838
}
39-
40-
/// <summary>
41-
/// Allows associating an annotation with a <see cref="CliSymbol"/>. The annotation will be stored by the accessor's owner <see cref="CliSubsystem"/>.
42-
/// </summary>
43-
public struct ValueAnnotationAccessor<TValue>(CliSubsystem owner, AnnotationId<TValue> id)
44-
{
45-
/// <summary>
46-
/// The ID of the annotation
47-
/// </summary>
48-
public AnnotationId<TValue> Id { get; }
49-
public readonly void Set<TSymbolValue>(CliOption<TSymbolValue> symbol, TSymbolValue value)
50-
where TSymbolValue : TValue
51-
=> owner.SetAnnotation(symbol, id, value);
52-
public readonly void Set<TSymbolValue>(CliArgument<TSymbolValue> symbol, TSymbolValue value)
53-
where TSymbolValue : TValue
54-
=> owner.SetAnnotation(symbol, id, value);
55-
public readonly bool TryGet(CliSymbol symbol, [NotNullWhen(true)] out TValue? value) => owner.TryGetAnnotation(symbol, id, out value);
56-
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
value = (TSymbolValue)storedValue;
58+
return true;
59+
}
60+
value = default;
61+
return false;
62+
}
63+
}
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
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.CommandLine.Parsing;
5-
64
namespace System.CommandLine.Subsystems.Annotations;
75

86
/// <summary>
97
/// IDs for well-known Version annotations.
108
/// </summary>
119
public static class ValueAnnotations
1210
{
13-
public static string Prefix { get; } = nameof(SubsystemKind.Value);
11+
internal static string Prefix { get; } = nameof(SubsystemKind.Value);
1412

13+
/// <summary>
14+
/// Provides Set and Get for default values
15+
/// </summary>
1516
public static AnnotationId<object?> DefaultValue { get; } = new(Prefix, nameof(DefaultValue));
16-
public static AnnotationId<Func<object?>?> DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation));
17-
public static AnnotationId<object?> Value { get; } = new(Prefix, nameof(Value));
17+
18+
/// <summary>
19+
/// Provides Set and Get for default value calculations
20+
/// </summary>
21+
public static AnnotationId<Func<object?>> DefaultValueCalculation { get; } = new(Prefix, nameof(DefaultValueCalculation));
1822
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 delegate must return a value 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 ValueFuncAnnotationAccessor<TValue>(CliSubsystem owner, AnnotationId<Func<TValue>> id)
18+
{
19+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Id"/>>
20+
public AnnotationId<Func<TValue>> Id { get; }
21+
22+
/// <inheritdoc cref="AnnotationAccessor{TValue}.Set"/>>
23+
public readonly void Set<TSymbolValue>(CliOption<TSymbolValue> symbol, Func<TValue> 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, Func<TValue> 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 Func<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 Func<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 Func<TValue>? value)
49+
where TSymbolValue : TValue
50+
=> TryGetInternal<TSymbolValue>(symbol, out value);
51+
52+
private readonly bool TryGetInternal<TSymbolValue>(CliSymbol symbol, [NotNullWhen(true)] out Func<TValue>? value)
53+
where TSymbolValue : TValue
54+
{
55+
if (owner.TryGetAnnotation(symbol, id, out Func<TValue>? storedValue))
56+
{
57+
value = storedValue;
58+
return true;
59+
}
60+
value = default;
61+
return false;
62+
}
63+
}

src/System.CommandLine.Subsystems/Subsystems/CliSubsystem.cs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ protected CliSubsystem(string name, SubsystemKind subsystemKind, IAnnotationProv
2222
/// The name of the subsystem.
2323
/// </summary>
2424
public string Name { get; }
25+
26+
/// <summary>
27+
/// Defines the kind of subsystem, such as help or version
28+
/// </summary>
2529
public SubsystemKind SubsystemKind { get; }
2630

27-
DefaultAnnotationProvider? _defaultProvider;
28-
readonly IAnnotationProvider? _annotationProvider;
31+
private DefaultAnnotationProvider? _defaultProvider;
32+
private readonly IAnnotationProvider? _annotationProvider;
2933

3034
/// <summary>
3135
/// Attempt to retrieve the value for the symbol and annotation ID from the annotation provider.
@@ -53,6 +57,24 @@ protected internal bool TryGetAnnotation<TValue>(CliSymbol symbol, AnnotationId<
5357
return false;
5458
}
5559

60+
/// <inheritdoc cref="CliSubsystem.SetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, TValue)"/>
61+
/// <returns>A delegate that returns the value.</returns>
62+
protected internal bool TryGetAnnotation<TValue>(CliSymbol symbol, AnnotationId<Func<TValue>> id, [NotNullWhen(true)] out Func<TValue>? value)
63+
{
64+
if (_defaultProvider is not null && _defaultProvider.TryGet(symbol, id, out var storedValue))
65+
{
66+
value = storedValue;
67+
return true;
68+
}
69+
if (_annotationProvider is not null && _annotationProvider.TryGet(symbol, id, out var storedValue2))
70+
{
71+
value = storedValue2;
72+
return true;
73+
}
74+
value = default;
75+
return false;
76+
}
77+
5678
/// <summary>
5779
/// Set the value for the symbol and annotation ID in the annotation provider.
5880
/// </summary>
@@ -69,24 +91,34 @@ protected internal void SetAnnotation<TValue>(CliSymbol symbol, AnnotationId<TVa
6991
(_defaultProvider ??= new DefaultAnnotationProvider()).Set(symbol, id, value);
7092
}
7193

94+
/// <inheritdoc cref="CliSubsystem.SetAnnotation{TValue}(CliSymbol, AnnotationId{TValue}, TValue)"/>
95+
/// <param name="value">A delegate that returns the value.</param>
96+
protected internal void SetAnnotation<TValue>(CliSymbol symbol, AnnotationId<Func<TValue>> id, Func<TValue> value)
97+
{
98+
(_defaultProvider ??= new DefaultAnnotationProvider()).Set<Func<TValue>>(symbol, id, value);
99+
}
100+
101+
/// <summary>
102+
/// The subystem executes, even if another subsystem has handled the operation. This is expected to be used in things like error reporting.
103+
/// </summary>
72104
protected internal virtual bool RunsEvenIfAlreadyHandled { get; protected set; }
73105

74106
/// <summary>
75107
/// Executes the behavior of the subsystem. For example, help would write information to the console.
76108
/// </summary>
77109
/// <param name="pipelineContext">The context contains data like the ParseResult, and allows setting of values like whether execution was handled and the CLI should terminate </param>
78110
/// <returns>A CliExit object with information such as whether the CLI should terminate</returns>
79-
protected internal virtual CliExit Execute(PipelineContext pipelineContext)
111+
protected internal virtual CliExit Execute(PipelineContext pipelineContext)
80112
=> CliExit.NotRun(pipelineContext.ParseResult);
81113

82114
internal PipelineContext ExecuteIfNeeded(PipelineContext pipelineContext)
83-
=> ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext);
115+
=> ExecuteIfNeeded(pipelineContext.ParseResult, pipelineContext);
84116

85117
internal PipelineContext ExecuteIfNeeded(ParseResult? parseResult, PipelineContext pipelineContext)
86118
{
87-
if( GetIsActivated(parseResult))
119+
if (GetIsActivated(parseResult))
88120
{
89-
Execute(pipelineContext );
121+
Execute(pipelineContext);
90122
}
91123
return pipelineContext;
92124
}
@@ -120,7 +152,7 @@ protected internal virtual CliConfiguration Initialize(InitializationContext con
120152
=> context.Configuration;
121153

122154
// TODO: Determine if this is needed.
123-
protected internal virtual CliExit TearDown(CliExit cliExit)
155+
protected internal virtual CliExit TearDown(CliExit cliExit)
124156
=> cliExit;
125157

126158
}

src/System.CommandLine.Subsystems/ValueSubsystem.cs

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,52 @@
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.CommandLine.Parsing;
54
using System.CommandLine.Subsystems;
65
using System.CommandLine.Subsystems.Annotations;
76

87
namespace System.CommandLine;
98

10-
public class ValueSubsystem : CliSubsystem
9+
public class ValueSubsystem(IAnnotationProvider? annotationProvider = null)
10+
: CliSubsystem(ValueAnnotations.Prefix, SubsystemKind.Value, annotationProvider)
1111
{
12-
// @mhutch: Is the TryGet on the sparse dictionaries how we should handle a case where the annotations will be sparse to support lazy? If so, should we have another method on
13-
// the annotation wrapper, or an alternative struct when there a TryGet makes sense? This API needs review, maybe next Tuesday.
14-
//private PipelineContext? pipelineContext = null;
15-
private Dictionary<CliSymbol, object?> cachedValues = new();
12+
private Dictionary<CliSymbol, object?> cachedValues = [];
1613
private ParseResult? parseResult = null;
1714

18-
public ValueSubsystem(IAnnotationProvider? annotationProvider = null)
19-
: base(ValueAnnotations.Prefix, SubsystemKind.Version, annotationProvider)
20-
{ }
21-
22-
//internal void SetDefaultValue(CliSymbol symbol, object? defaultValue)
23-
// => SetAnnotation(symbol, ValueAnnotations.DefaultValue, defaultValue);
24-
//internal object? GetDefaultValue(CliSymbol symbol)
25-
// => TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var defaultValue)
26-
// ? defaultValue
27-
// : "";
28-
private bool TryGetDefaultValue<T>(CliSymbol symbol, out T? defaultValue)
29-
{
30-
if (TryGetAnnotation(symbol, ValueAnnotations.DefaultValue, out var objectValue))
31-
{
32-
defaultValue = (T)objectValue;
33-
return true;
34-
}
35-
defaultValue = default;
36-
return false;
37-
}
38-
public AnnotationAccessor<object?> DefaultValue
15+
/// <summary>
16+
/// Provides access to Get and Set methods for default values for symbols
17+
/// </summary>
18+
public ValueAnnotationAccessor<object?> DefaultValue
3919
=> new(this, ValueAnnotations.DefaultValue);
4020

41-
internal void SetDefaultValueCalculation(CliSymbol symbol, Func<object?> factory)
42-
=> SetAnnotation(symbol, ValueAnnotations.DefaultValueCalculation, factory);
43-
internal Func<object?>? GetDefaultValueCalculation(CliSymbol symbol)
44-
=> TryGetAnnotation<Func<object?>?>(symbol, ValueAnnotations.DefaultValueCalculation, out var value)
45-
? value
46-
: null;
47-
public AnnotationAccessor<Func<object?>?> DefaultValueCalculation
48-
=> new(this, ValueAnnotations.DefaultValueCalculation);
21+
/// <summary>
22+
/// Provides access to Get and Set methods for default value calculations for symbols
23+
/// </summary>
24+
public ValueFuncAnnotationAccessor<object?> DefaultValueCalculation
25+
=> new (this, ValueAnnotations.DefaultValueCalculation);
4926

27+
// It is possible that another subsystems GetIsActivated method will access a value.
28+
// If this is called from a GetIsActivated method of a subsystem in the early termination group,
29+
// it will fail. That is not an expected scenario.
30+
/// <inheritdoc cref="CliSubsystem.GetIsActivated"/>
31+
/// <remarks>
32+
/// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior
33+
/// </remarks>
5034
protected internal override bool GetIsActivated(ParseResult? parseResult)
5135
{
5236
this.parseResult = parseResult;
5337
return true;
5438
}
5539

40+
/// <inheritdoc cref="CliSubsystem.Execute"/>
41+
/// <remarks>
42+
/// Note to inheritors: Call base for all ValueSubsystem methods that you override to ensure correct behavior
43+
/// </remarks>
5644
protected internal override CliExit Execute(PipelineContext pipelineContext)
5745
{
5846
parseResult ??= pipelineContext.ParseResult;
5947
return base.Execute(pipelineContext);
6048
}
6149

62-
// TODO: Do it! Consider using a simple dictionary instead of the annotation (@mhutch) because with is not useful here
6350
private void SetValue<T>(CliSymbol symbol, object? value)
6451
=> cachedValues.Add(symbol, value);
6552
private bool TryGetValue<T>(CliSymbol symbol, out T? value)
@@ -78,7 +65,7 @@ private bool TryGetValue<T>(CliSymbol symbol, out T? value)
7865
public T? GetValue<T>(CliOption option)
7966
=> GetValueInternal<T>(option);
8067
public T? GetValue<T>(CliArgument argument)
81-
=> GetValueInternal<T>(argument);
68+
=> GetValueInternal<T>(argument);
8269

8370
private T? GetValueInternal<T>(CliSymbol? symbol)
8471
=> symbol switch
@@ -90,10 +77,10 @@ not null when TryGetValue<T>(symbol, out var value)
9077
CliOption option when parseResult?.GetValueResult(option) is {} valueResult // GetValue not used because it would always return a value
9178
=> UseValue(symbol, valueResult.GetValue<T>()), // Value was supplied during parsing
9279
// Value was not supplied during parsing, determine default now
93-
not null when DefaultValueCalculation.TryGet(symbol, out var defaultValueCalculation)
80+
not null when DefaultValueCalculation.TryGet<T>(symbol, out var defaultValueCalculation)
9481
=> UseValue(symbol, CalculatedDefault<T>(symbol, defaultValueCalculation)),
95-
not null when TryGetDefaultValue<T>(symbol, out var explicitValue)
96-
=> UseValue(symbol, explicitValue),
82+
not null when DefaultValue.TryGet<T>(symbol, out var explicitValue)
83+
=> UseValue<T>(symbol, (T)explicitValue),
9784
//not null when GetDefaultFromEnvironmentVariable<T>(symbol, out var envName)
9885
// => UseValue(symbol, GetEnvByName(envName)),
9986
null => throw new ArgumentNullException(nameof(symbol)),

0 commit comments

Comments
 (0)