forked from natemcmaster/CommandLineUtils
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce mapped options as defined in natemcmaster#334
- Loading branch information
1 parent
6ccfee6
commit 586c3c1
Showing
9 changed files
with
966 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// Copyright (c) Nate McMaster. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Reflection; | ||
|
||
namespace McMaster.Extensions.CommandLineUtils.Attributes | ||
{ | ||
/// <summary> | ||
/// Represents one or many command line option that is identified by flag proceeded by '-' or '--'. | ||
/// Options are not positional. Compare to <see cref="ArgumentAttribute"/>. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property)] | ||
public sealed class MappedOptionAttribute : OptionAttributeBase | ||
{ | ||
/// <summary> | ||
/// Initializes a new <see cref="MappedOptionAttribute"/>. | ||
/// </summary> | ||
public MappedOptionAttribute(object constantValue) | ||
{ | ||
ConstantValue = constantValue; | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new <see cref="MappedOptionAttribute"/>. | ||
/// </summary> | ||
/// <param name="template">The string template. This is parsed into <see cref="CommandOption.ShortName"/> and <see cref="CommandOption.LongName"/>.</param> | ||
/// <param name="constantValue">The value to assign the the option is given.</param> | ||
public MappedOptionAttribute(string template, object constantValue) | ||
{ | ||
Template = template; | ||
ConstantValue = constantValue; | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new <see cref="MappedOptionAttribute"/>. | ||
/// </summary> | ||
/// <param name="template">The template</param> | ||
/// <param name="description">The option description</param> | ||
/// <param name="constantValue">The value to assign the the option is given.</param> | ||
public MappedOptionAttribute(string template, string? description, object constantValue) | ||
{ | ||
Template = template; | ||
Description = description; | ||
ConstantValue = constantValue; | ||
} | ||
|
||
/// <summary> | ||
/// Defines the type of the option. When not set, this will be inferred from the CLR type of the property. | ||
/// </summary> | ||
/// <seealso cref="CommandOption.OptionType"/> | ||
public CommandOptionType? OptionType { get; set; } | ||
|
||
/// <summary> | ||
/// Defines the value assigned to the property when the option is given. | ||
/// </summary> | ||
/// <seealso cref="ConstantValueOption{T}.ConstantValue"/> | ||
public object? ConstantValue { get; set; } | ||
|
||
internal CommandOption Configure<T>(MappedOption<T> mappedOption, PropertyInfo prop) | ||
{ | ||
T constantValue; | ||
try | ||
{ | ||
constantValue = (T)ConstantValue!; | ||
} | ||
catch (InvalidCastException e) | ||
{ | ||
throw new InvalidOperationException(Strings.CannotDetermineOptionType(prop), e); | ||
} | ||
ConstantValueOption<T> option; | ||
if (Template != null) | ||
{ | ||
option = mappedOption.Add(Template, constantValue); | ||
} | ||
else | ||
{ | ||
option = mappedOption.Add(constantValue); | ||
var stringValue = constantValue?.ToString().ToKebabCase(); | ||
if (stringValue != null) | ||
{ | ||
option.LongName = stringValue; | ||
option.ShortName = stringValue.Substring(0, 1); | ||
} | ||
} | ||
|
||
Configure(option); | ||
return option; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// Copyright (c) Nate McMaster. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Linq; | ||
|
||
namespace McMaster.Extensions.CommandLineUtils | ||
{ | ||
/// <summary> | ||
/// An option that returns a constant <see cref="ParsedValue"/> when specified on the command-line | ||
/// </summary> | ||
/// <typeparam name="T">type of <see cref="ConstantValue"/></typeparam> | ||
public class ConstantValueOption<T> : CommandOption, IOption<T>, IInternalCommandParamOfT | ||
{ | ||
private readonly List<T> _parsedValues = new List<T>(); | ||
private readonly T _constantValue; | ||
private bool _hasBeenParsed; | ||
|
||
/// <summary> | ||
/// Initializes a new <see cref="ConstantValueOption{T}"/> | ||
/// </summary> | ||
/// <param name="template">The option template.</param> | ||
/// <param name="constantValue">The value to return is specified on the command line</param> | ||
/// <see cref="MappedOption{T}"/> | ||
public ConstantValueOption(string template, T constantValue) : base(template, CommandOptionType.NoValue) | ||
{ | ||
_constantValue = constantValue; | ||
UnderlyingType = typeof(T); | ||
} | ||
|
||
internal ConstantValueOption(T constantValue) : base(CommandOptionType.NoValue) | ||
{ | ||
_constantValue = constantValue; | ||
UnderlyingType = typeof(T); | ||
} | ||
|
||
/// <summary> | ||
/// The value that is returned for any usage of this option | ||
/// </summary> | ||
public T ConstantValue => _constantValue; | ||
|
||
/// <summary> | ||
/// The parsed value. | ||
/// </summary> | ||
public T ParsedValue => ParsedValues.FirstOrDefault(); | ||
|
||
/// <summary> | ||
/// All parsed values; | ||
/// </summary> | ||
public IReadOnlyList<T> ParsedValues | ||
{ | ||
get | ||
{ | ||
if (!_hasBeenParsed) | ||
{ | ||
((IInternalCommandParamOfT)this).Parse(CultureInfo.CurrentCulture); | ||
} | ||
|
||
return _parsedValues; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// "Parse" the user-given values into the constant value | ||
/// </summary> | ||
void IInternalCommandParamOfT.Parse(CultureInfo culture) | ||
{ | ||
_hasBeenParsed = true; | ||
_parsedValues.Clear(); | ||
foreach (var t in base._values) | ||
{ | ||
_parsedValues.Add(_constantValue); | ||
} | ||
} | ||
|
||
/// <inheritdoc/> | ||
public override void Reset() | ||
{ | ||
_hasBeenParsed = false; | ||
_parsedValues.Clear(); | ||
base.Reset(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
src/CommandLineUtils/Conventions/MappedOptionAttributeConvention.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// Copyright (c) Nate McMaster. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.ExceptionServices; | ||
using McMaster.Extensions.CommandLineUtils.Abstractions; | ||
using McMaster.Extensions.CommandLineUtils.Attributes; | ||
|
||
namespace McMaster.Extensions.CommandLineUtils.Conventions | ||
{ | ||
/// <summary> | ||
/// Adds an <see cref="CommandOption"/> to match each usage of <see cref="MappedOptionAttribute"/> | ||
/// on the model type of <see cref="CommandLineApplication{TModel}"/>. | ||
/// </summary> | ||
public class MappedOptionAttributeConvention : OptionAttributeConventionBase<MappedOptionAttribute>, IConvention | ||
{ | ||
/// <inheritdoc /> | ||
public virtual void Apply(ConventionContext context) | ||
{ | ||
if (context.ModelType == null) | ||
{ | ||
return; | ||
} | ||
|
||
var props = ReflectionHelper.GetProperties(context.ModelType); | ||
foreach (var prop in props) | ||
{ | ||
using var enumerator = prop.GetCustomAttributes<MappedOptionAttribute>().GetEnumerator(); | ||
if (!enumerator.MoveNext()) | ||
{ | ||
continue; | ||
} | ||
|
||
EnsureDoesNotHaveHelpOptionAttribute(prop); | ||
EnsureDoesNotHaveVersionOptionAttribute(prop); | ||
EnsureDoesNotHaveArgumentAttribute(prop); | ||
|
||
var modelAccessor = context.ModelAccessor; | ||
if (modelAccessor == null) | ||
{ | ||
throw new InvalidOperationException(Strings.ConventionRequiresModel); | ||
} | ||
|
||
var method = s_applyGeneric.MakeGenericMethod(context.ModelType, prop.PropertyType); | ||
try | ||
{ | ||
method.Invoke(this, new object[] { context, prop, enumerator }); | ||
} | ||
catch (TargetInvocationException e) | ||
{ | ||
var innerException = e.InnerException; | ||
if (innerException != null) | ||
{ | ||
ExceptionDispatchInfo.Capture(innerException).Throw(); | ||
} | ||
throw; | ||
} | ||
} | ||
} | ||
|
||
private static readonly MethodInfo s_applyGeneric | ||
= typeof(MappedOptionAttributeConvention) | ||
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) | ||
.Single(m => m.Name == nameof(Apply) && m.IsGenericMethod); | ||
|
||
private void Apply<TModel, TValue>(ConventionContext context, PropertyInfo prop, | ||
IEnumerator<MappedOptionAttribute> enumerator) | ||
where TModel : class | ||
{ | ||
var setterDelegate = ReflectionHelper.GetPropertySetter(prop); | ||
|
||
var option = new SingularPropertyMappedOption<TModel, TValue>(context.Application, setterDelegate) | ||
{ | ||
Description = prop.Name | ||
}; | ||
context.Application.AddOption(option); | ||
do | ||
{ | ||
enumerator.Current.Configure(option, prop); | ||
} while (enumerator.MoveNext()); | ||
} | ||
|
||
private static void EnsureDoesNotHaveVersionOptionAttribute(PropertyInfo prop) | ||
{ | ||
var versionOptionAttr = prop.GetCustomAttribute<VersionOptionAttribute>(); | ||
if (versionOptionAttr != null) | ||
{ | ||
throw new InvalidOperationException( | ||
Strings.BothHelpOptionAndVersionOptionAttributesCannotBeSpecified(prop)); | ||
} | ||
} | ||
|
||
private static void EnsureDoesNotHaveHelpOptionAttribute(PropertyInfo prop) | ||
{ | ||
var versionOptionAttr = prop.GetCustomAttribute<VersionOptionAttribute>(); | ||
if (versionOptionAttr != null) | ||
{ | ||
throw new InvalidOperationException( | ||
Strings.BothHelpOptionAndVersionOptionAttributesCannotBeSpecified(prop)); | ||
} | ||
} | ||
|
||
class SingularPropertyMappedOption<TModel, TValue> | ||
: MappedOption<TValue>, IInternalCommandParamOfT | ||
where TModel : class | ||
{ | ||
private readonly SetPropertyDelegate _propertySetter; | ||
|
||
public SingularPropertyMappedOption(CommandLineApplication commandLineApplication, | ||
SetPropertyDelegate propertySetter) | ||
: base(commandLineApplication, CommandOptionType.SingleValue) | ||
{ | ||
_propertySetter = propertySetter; | ||
} | ||
|
||
public void Parse(CultureInfo culture) | ||
{ | ||
if (!HasValue()) | ||
{ | ||
return; | ||
} | ||
|
||
switch (_commandLineApplication) | ||
{ | ||
case CommandLineApplication<TModel> appT: | ||
{ | ||
_propertySetter(appT.Model, ParsedValue); | ||
break; | ||
} | ||
case IModelAccessor modelAccessor: | ||
{ | ||
_propertySetter((TModel)modelAccessor.GetModel(), ParsedValue); | ||
break; | ||
} | ||
default: | ||
{ | ||
throw new InvalidOperationException("Can not get the model of the current command"); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.