Skip to content

Commit 48e52f6

Browse files
authored
Add support for merged commands (#78)
1 parent 219cefd commit 48e52f6

16 files changed

+223
-36
lines changed

src/NClap/Metadata/CommandGroup`1.cs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class CommandGroup<TCommandType> : Command, IArgumentProvider, ICommandGr
1919
{
2020
private readonly CommandGroupOptions _options;
2121
private readonly object _parentObject;
22-
private TCommandType? _selectedCommandType;
22+
private object _selectedCommand;
2323

2424
/// <summary>
2525
/// Parameterless constructor. Deprecated.
@@ -68,17 +68,17 @@ public CommandGroup(TCommandType selection, object parentObject)
6868
/// <param name="selection">The selected command type.</param>
6969
/// <param name="parentObject">Optionally provides a reference to the
7070
/// object containing this command group.</param>
71-
public CommandGroup(CommandGroupOptions options, TCommandType selection, object parentObject) : this(options)
71+
public CommandGroup(CommandGroupOptions options, object selection, object parentObject) : this(options)
7272
{
7373
_parentObject = parentObject;
74-
Selection = selection;
74+
SelectedCommand = selection;
7575
}
7676

7777
/// <summary>
7878
/// True if the group has a selection, false if no selection was yet
7979
/// made.
8080
/// </summary>
81-
public bool HasSelection => Selection.HasValue;
81+
public bool HasSelection => SelectedCommand != null;
8282

8383
/// <summary>
8484
/// The enum value corresponding with the selected command, or null if no
@@ -87,19 +87,15 @@ public CommandGroup(CommandGroupOptions options, TCommandType selection, object
8787
[PositionalArgument(ArgumentFlags.Required, Position = 0, LongName = nameof(Command))]
8888
public TCommandType? Selection
8989
{
90-
get => _selectedCommandType;
91-
set
92-
{
93-
_selectedCommandType = value;
94-
InstantiatedCommand = value.HasValue ? InstantiateCommand(value.Value) : null;
95-
}
90+
get => (TCommandType)SelectedCommand;
91+
set => SelectedCommand = value.HasValue ? (object)value.Value : null;
9692
}
9793

9894
/// <summary>
9995
/// The enum value corresponding with the selected command, or null if no
10096
/// selection has yet been made.
10197
/// </summary>
102-
object ICommandGroup.Selection => Selection;
98+
object ICommandGroup.Selection => SelectedCommand;
10399

104100
/// <summary>
105101
/// The command presently selected from this group, or null if no
@@ -149,7 +145,17 @@ public override Task<CommandResult> ExecuteAsync(CancellationToken cancel)
149145
return InstantiatedCommand.ExecuteAsync(cancel);
150146
}
151147

152-
private ICommand InstantiateCommand(TCommandType selection)
148+
private object SelectedCommand
149+
{
150+
get => _selectedCommand;
151+
set
152+
{
153+
_selectedCommand = value;
154+
InstantiatedCommand = value != null ? InstantiateCommand(value) : null;
155+
}
156+
}
157+
158+
private ICommand InstantiateCommand(object selection)
153159
{
154160
var commandTypeType = EnumArgumentType.Create(typeof(TCommandType));
155161

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using NClap.Types;
3+
4+
namespace NClap.Metadata
5+
{
6+
/// <summary>
7+
/// Attribute that indicates the associated enum type is extensible.
8+
/// </summary>
9+
[AttributeUsage(AttributeTargets.Enum, AllowMultiple = true)]
10+
public sealed class ExtensibleEnumAttribute : Attribute
11+
{
12+
/// <summary>
13+
/// Constructor.
14+
/// </summary>
15+
/// <param name="provider">Provider.</param>
16+
public ExtensibleEnumAttribute(Type provider)
17+
{
18+
Provider = provider;
19+
}
20+
21+
/// <summary>
22+
/// Implementation of <see cref="IEnumArgumentTypeProvider"/>.
23+
/// </summary>
24+
public Type Provider { get; set; }
25+
}
26+
}

src/NClap/PublicAPI.Unshipped.txt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ NClap.Help.ArgumentHelpOptions.IncludeNamedArgumentValueSyntax.set -> void
66
NClap.Metadata.ArgumentSetAttribute.ExpandLogo.get -> bool
77
NClap.Metadata.ArgumentSetAttribute.ExpandLogo.set -> void
88
NClap.Metadata.CommandGroup<TCommandType>.CommandGroup(NClap.Metadata.CommandGroupOptions options) -> void
9-
NClap.Metadata.CommandGroup<TCommandType>.CommandGroup(NClap.Metadata.CommandGroupOptions options, TCommandType selection, object parentObject) -> void
9+
NClap.Metadata.CommandGroup<TCommandType>.CommandGroup(NClap.Metadata.CommandGroupOptions options, object selection, object parentObject) -> void
1010
NClap.Metadata.CommandGroupOptions
1111
NClap.Metadata.CommandGroupOptions.DeepClone() -> NClap.Metadata.CommandGroupOptions
12+
NClap.Metadata.ExtensibleEnumAttribute
13+
NClap.Metadata.ExtensibleEnumAttribute.ExtensibleEnumAttribute(System.Type provider) -> void
14+
NClap.Metadata.ExtensibleEnumAttribute.Provider.get -> System.Type
15+
NClap.Metadata.ExtensibleEnumAttribute.Provider.set -> void
1216
NClap.Parser.ArgumentDefinition.ArgumentDefinition(System.Reflection.MemberInfo member, NClap.Metadata.ArgumentBaseAttribute attribute, NClap.Parser.ArgumentSetDefinition argSet, object defaultValue = null, NClap.Parser.ArgumentDefinition containingArgument = null, NClap.ServiceConfigurer serviceConfigurer = null) -> void
1317
NClap.Parser.AttributeBasedArgumentDefinitionFactory
1418
NClap.Repl.ILoopClient.PromptWithColor.get -> NClap.Utilities.ColoredString?
@@ -26,14 +30,16 @@ NClap.ServiceConfigurer
2630
NClap.Types.ArgumentParseContext.ServiceConfigurer.get -> NClap.ServiceConfigurer
2731
NClap.Types.ArgumentParseContext.ServiceConfigurer.set -> void
2832
NClap.Types.IArgumentValue.GetAttributes<T>() -> System.Collections.Generic.IEnumerable<T>
33+
NClap.Types.IEnumArgumentTypeProvider
34+
NClap.Types.IEnumArgumentTypeProvider.GetTypes() -> System.Collections.Generic.IEnumerable<NClap.Types.IEnumArgumentType>
2935
static NClap.CommandLineParser.Format<T>(T value, NClap.CommandLineParserOptions options) -> System.Collections.Generic.IEnumerable<string>
36+
static NClap.CommandLineParser.GetCompletions(System.Type type, string commandLineToComplete, int charIndexOfCursor) -> System.Collections.Generic.IEnumerable<string>
37+
static NClap.CommandLineParser.GetCompletions(System.Type type, string commandLineToComplete, int charIndexOfCursor, int tokensToSkip, NClap.CommandLineParserOptions options) -> System.Collections.Generic.IEnumerable<string>
3038
static NClap.CommandLineParser.GetUsageInfo(System.Type type, NClap.Help.ArgumentSetHelpOptions options = null, object defaultValues = null, NClap.ServiceConfigurer serviceConfigurer = null) -> NClap.Utilities.ColoredMultistring
3139
static NClap.CommandLineParserOptionsExtensions.ConfigureServices(this NClap.Utilities.FluentBuilder<NClap.CommandLineParserOptions> builder, NClap.ServiceConfigurer configurer) -> NClap.Utilities.FluentBuilder<NClap.CommandLineParserOptions>
3240
static NClap.CommandLineParserOptionsExtensions.Quiet(this NClap.Utilities.FluentBuilder<NClap.CommandLineParserOptions> builder) -> NClap.Utilities.FluentBuilder<NClap.CommandLineParserOptions>
3341
static NClap.CommandLineParserOptionsExtensions.TryParse<T>(this NClap.CommandLineParserOptions options, System.Collections.Generic.IEnumerable<string> arguments, out T result) -> bool
3442
static NClap.CommandLineParserOptionsExtensions.With(this NClap.CommandLineParserOptions options) -> NClap.Utilities.FluentBuilder<NClap.CommandLineParserOptions>
35-
static NClap.CommandLineParser.GetCompletions(System.Type type, string commandLineToComplete, int charIndexOfCursor) -> System.Collections.Generic.IEnumerable<string>
36-
static NClap.CommandLineParser.GetCompletions(System.Type type, string commandLineToComplete, int charIndexOfCursor, int tokensToSkip, NClap.CommandLineParserOptions options) -> System.Collections.Generic.IEnumerable<string>
3743
static NClap.Help.ArgumentSetHelpOptionsExtensions.NoDescription(this NClap.Utilities.FluentBuilder<NClap.Help.ArgumentSetHelpOptions> builder) -> NClap.Utilities.FluentBuilder<NClap.Help.ArgumentSetHelpOptions>
3844
static NClap.Help.ArgumentSetHelpOptionsExtensions.NoEnumValues(this NClap.Utilities.FluentBuilder<NClap.Help.ArgumentSetHelpOptions> builder) -> NClap.Utilities.FluentBuilder<NClap.Help.ArgumentSetHelpOptions>
3945
static NClap.Help.ArgumentSetHelpOptionsExtensions.NoExamples(this NClap.Utilities.FluentBuilder<NClap.Help.ArgumentSetHelpOptions> builder) -> NClap.Utilities.FluentBuilder<NClap.Help.ArgumentSetHelpOptions>

src/NClap/Types/CommandGroupArgumentType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ protected override object Parse(ArgumentParseContext context, string stringToPar
9595
ServiceConfigurer = context.ServiceConfigurer
9696
};
9797

98-
var constructorArgTypes = new[] { typeof(CommandGroupOptions), _commandTypeType, typeof(object) };
98+
var constructorArgTypes = new[] { typeof(CommandGroupOptions), typeof(object), typeof(object) };
9999
var commandGroupConstructor = Type.GetTypeInfo().GetConstructor(constructorArgTypes);
100100

101101
if (commandGroupConstructor == null)

src/NClap/Types/EnumArgumentType.cs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Globalization;
44
using System.Linq;
55
using System.Reflection;
6+
using NClap.Metadata;
67
using NClap.Utilities;
78

89
namespace NClap.Types
@@ -12,15 +13,15 @@ namespace NClap.Types
1213
/// </summary>
1314
internal class EnumArgumentType : ArgumentTypeBase, IEnumArgumentType
1415
{
15-
private readonly Dictionary<string, EnumArgumentValue> _valuesByCaseSensitiveName =
16-
new Dictionary<string, EnumArgumentValue>(StringComparer.Ordinal);
16+
private readonly Dictionary<string, IArgumentValue> _valuesByCaseSensitiveName =
17+
new Dictionary<string, IArgumentValue>(StringComparer.Ordinal);
1718

18-
private readonly Dictionary<string, EnumArgumentValue> _valuesByCaseInsensitiveName =
19-
new Dictionary<string, EnumArgumentValue>(StringComparer.OrdinalIgnoreCase);
19+
private readonly Dictionary<string, IArgumentValue> _valuesByCaseInsensitiveName =
20+
new Dictionary<string, IArgumentValue>(StringComparer.OrdinalIgnoreCase);
2021

21-
private readonly Dictionary<object, EnumArgumentValue> _valuesByValue = new Dictionary<object, EnumArgumentValue>();
22+
private readonly Dictionary<object, IArgumentValue> _valuesByValue = new Dictionary<object, IArgumentValue>();
2223

23-
private readonly List<EnumArgumentValue> _values = new List<EnumArgumentValue>();
24+
private readonly List<IArgumentValue> _values = new List<IArgumentValue>();
2425

2526
/// <summary>
2627
/// Constructs an object to describe an empty enumeration type. Values must be
@@ -49,7 +50,20 @@ protected EnumArgumentType(Type type) : base(type)
4950
public static EnumArgumentType Create(Type type)
5051
{
5152
var flagsAttrib = type.GetTypeInfo().GetSingleAttribute<FlagsAttribute>();
52-
return (flagsAttrib != null) ? new FlagsEnumArgumentType(type) : new EnumArgumentType(type);
53+
var argType = (flagsAttrib != null) ? new FlagsEnumArgumentType(type) : new EnumArgumentType(type);
54+
55+
var extensibleAttribs = type.GetTypeInfo().GetAttributes<ExtensibleEnumAttribute>().ToList();
56+
if (extensibleAttribs.Count > 0)
57+
{
58+
var types = extensibleAttribs.Select(a => a.Provider)
59+
.Where(p => p != null)
60+
.Select(ConstructEnumArgumentTypeProviderFromType)
61+
.SelectMany(p => p.GetTypes());
62+
63+
argType = new MergedEnumArgumentType(new[] { argType }.Concat(types));
64+
}
65+
66+
return argType;
5367
}
5468

5569
/// <summary>
@@ -64,7 +78,7 @@ public override string Format(object value)
6478
{
6579
if (value == null) throw new ArgumentNullException(nameof(value));
6680

67-
if (_valuesByValue.TryGetValue(value, out EnumArgumentValue enumValue))
81+
if (_valuesByValue.TryGetValue(value, out IArgumentValue enumValue))
6882
{
6983
return enumValue.DisplayName;
7084
}
@@ -102,7 +116,7 @@ protected override object Parse(ArgumentParseContext context, string stringToPar
102116
var map = context.CaseSensitive ? _valuesByCaseSensitiveName : _valuesByCaseInsensitiveName;
103117

104118
// First try looking up the string in our name map.
105-
if (!map.TryGetValue(stringToParse, out EnumArgumentValue value))
119+
if (!map.TryGetValue(stringToParse, out IArgumentValue value))
106120
{
107121
// We might have more options if it's an enum type, but if it's not--there's
108122
// nothing else we can do.
@@ -145,7 +159,7 @@ protected override object Parse(ArgumentParseContext context, string stringToPar
145159
/// <returns>true on success; false otherwise.</returns>
146160
public bool TryGetValue(object value, out IArgumentValue argValue)
147161
{
148-
if (!_valuesByValue.TryGetValue(value, out EnumArgumentValue enumArgValue))
162+
if (!_valuesByValue.TryGetValue(value, out IArgumentValue enumArgValue))
149163
{
150164
argValue = null;
151165
return false;
@@ -159,7 +173,7 @@ public bool TryGetValue(object value, out IArgumentValue argValue)
159173
/// Defines a new value in this type.
160174
/// </summary>
161175
/// <param name="value">Value to define.</param>
162-
protected void AddValue(EnumArgumentValue value)
176+
protected void AddValue(IArgumentValue value)
163177
{
164178
_values.Add(value);
165179
AddToValueNameMap(_valuesByCaseSensitiveName, value);
@@ -184,10 +198,22 @@ protected void AddValuesFromType(Type type)
184198
}
185199
}
186200

187-
private static IEnumerable<EnumArgumentValue> GetAllValues(Type type) =>
201+
/// <summary>
202+
/// Defines all values in the given type.
203+
/// </summary>
204+
/// <param name="type">Type to inspect.</param>
205+
protected void AddValuesFromType(IEnumArgumentType type)
206+
{
207+
foreach (var value in type.GetValues())
208+
{
209+
AddValue(value);
210+
}
211+
}
212+
213+
private static IEnumerable<IArgumentValue> GetAllValues(Type type) =>
188214
type.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Static).Select(f => new EnumArgumentValue(f));
189215

190-
private static void AddToValueMap(Dictionary<object, EnumArgumentValue> map, EnumArgumentValue value)
216+
private static void AddToValueMap(Dictionary<object, IArgumentValue> map, IArgumentValue value)
191217
{
192218
// We do our best to add each value to the map; but if there
193219
// are multiple members that share a value, then the first
@@ -204,7 +230,7 @@ private static void AddToValueMap(Dictionary<object, EnumArgumentValue> map, Enu
204230
/// </summary>
205231
/// <param name="map">Map to add to.</param>
206232
/// <param name="value">The value to add.</param>
207-
private static void AddToValueNameMap(Dictionary<string, EnumArgumentValue> map, EnumArgumentValue value)
233+
private static void AddToValueNameMap(Dictionary<string, IArgumentValue> map, IArgumentValue value)
208234
{
209235
// We skip disallowed values.
210236
if (value.Disallowed) return;
@@ -229,5 +255,10 @@ private static void AddToValueNameMap(Dictionary<string, EnumArgumentValue> map,
229255
map[value.ShortName] = value;
230256
}
231257
}
258+
259+
private static IEnumArgumentTypeProvider ConstructEnumArgumentTypeProviderFromType(Type type)
260+
{
261+
return (IEnumArgumentTypeProvider)type.GetParameterlessConstructor().Invoke(Array.Empty<object>());
262+
}
232263
}
233264
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Collections.Generic;
2+
3+
namespace NClap.Types
4+
{
5+
/// <summary>
6+
/// Enum argument type provider.
7+
/// </summary>
8+
public interface IEnumArgumentTypeProvider
9+
{
10+
/// <summary>
11+
/// Retrieves types being provided.
12+
/// </summary>
13+
/// <returns>Enumeration of types.</returns>
14+
IEnumerable<IEnumArgumentType> GetTypes();
15+
}
16+
}

src/NClap/Types/MergedEnumArgumentType.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,27 @@ public MergedEnumArgumentType(IEnumerable<Type> types)
2929
throw new ArgumentOutOfRangeException(nameof(types));
3030
}
3131
}
32+
33+
/// <summary>
34+
/// Constructs a type that merges the given enum types.
35+
/// </summary>
36+
/// <param name="types">Types to merge.</param>
37+
/// <exception cref="ArgumentOutOfRangeException">Thrown when zero types are
38+
/// provided.</exception>
39+
public MergedEnumArgumentType(IEnumerable<IEnumArgumentType> types)
40+
{
41+
var typeCount = 0;
42+
foreach (var type in types)
43+
{
44+
AddValuesFromType(type);
45+
++typeCount;
46+
}
47+
48+
// We throw if 0 types are merged.
49+
if (typeCount == 0)
50+
{
51+
throw new ArgumentOutOfRangeException(nameof(types));
52+
}
53+
}
3254
}
3355
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using NClap.Metadata;
2+
3+
namespace MergedCommandApp
4+
{
5+
[ExtensibleEnum(typeof(EnumProvider))]
6+
internal enum CommandType
7+
{
8+
[Command(typeof(SimpleCommand), Description = "Keep it simple")]
9+
Foo
10+
}
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Collections.Generic;
2+
using NClap.Types;
3+
4+
namespace MergedCommandApp
5+
{
6+
internal class EnumProvider : IEnumArgumentTypeProvider
7+
{
8+
public IEnumerable<IEnumArgumentType> GetTypes()
9+
{
10+
return new[] { (IEnumArgumentType)ArgumentType.GetType(typeof(ExtraCommandsType)) };
11+
}
12+
}
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using NClap.Metadata;
3+
4+
namespace MergedCommandApp
5+
{
6+
internal class ExtraCommand : SynchronousCommand
7+
{
8+
public override CommandResult Execute()
9+
{
10+
Console.WriteLine("Extra!");
11+
return CommandResult.Success;
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)