Skip to content

Commit

Permalink
Remove ReflectionAppBuilder and refactor its behavior into conventions
Browse files Browse the repository at this point in the history
  • Loading branch information
natemcmaster committed Mar 5, 2018
1 parent 0b789dd commit d9cb6d3
Show file tree
Hide file tree
Showing 36 changed files with 1,293 additions and 1,127 deletions.
8 changes: 7 additions & 1 deletion src/CommandLineUtils/Attributes/SubcommandAttribute.cs
Expand Up @@ -11,6 +11,8 @@ namespace McMaster.Extensions.CommandLineUtils
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class SubcommandAttribute : Attribute
{
private Type _commandType;

/// <summary>
/// Initializes a new instance of <see cref="SubcommandAttribute" />.
/// </summary>
Expand All @@ -30,7 +32,11 @@ public SubcommandAttribute(string name, Type commandType)
/// <summary>
/// The type of the subcommand.
/// </summary>
public Type CommandType { get; set; }
public Type CommandType
{
get => _commandType;
set => _commandType = value ?? throw new ArgumentNullException(nameof(value));
}

internal void Configure(CommandLineApplication app)
{
Expand Down
Expand Up @@ -56,7 +56,7 @@ internal CommandOption Configure(CommandLineApplication app, Type type, Func<obj

shortFormGetter = () =>
{
return methods[0].Invoke(targetInstanceFactory(), Constants.EmptyArray) as string;
return methods[0].Invoke(targetInstanceFactory?.Invoke(), Constants.EmptyArray) as string;
};
}

Expand Down
190 changes: 52 additions & 138 deletions src/CommandLineUtils/CommandLineApplication.Execute.cs
Expand Up @@ -18,84 +18,85 @@ namespace McMaster.Extensions.CommandLineUtils
partial class CommandLineApplication
{
/// <summary>
/// Creates an instance of <typeparamref name="TApp"/>, matching <paramref name="args"/>
/// Creates an instance of <typeparamref name="TApp"/>, matching <see cref="CommandLineContext.Arguments"/>
/// to all attributes on the type, and then invoking a method named "OnExecute" or "OnExecuteAsync" if it exists.
/// See <seealso cref="OptionAttribute" />, <seealso cref="ArgumentAttribute" />,
/// <seealso cref="HelpOptionAttribute"/>, and <seealso cref="VersionOptionAttribute"/>.
/// </summary>
/// <param name="args">The arguments</param>
/// <param name="context">The execution context.</param>
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static int Execute<TApp>(params string[] args)
public static int Execute<TApp>(CommandLineContext context)
where TApp : class, new()
=> Execute<TApp>(PhysicalConsole.Singleton, args);
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Arguments == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Arguments));
}

if (context.WorkingDirectory == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.WorkingDirectory));
}

if (context.Console == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Console));
}

try
{
using (var app = new CommandLineApplication<TApp>())
{
app.SetContext(context);
app.Conventions.UseDefaultConventions();
return app.Execute(context.Arguments);
}
}
catch (CommandParsingException ex)
{
context.Console.Error.WriteLine(ex.Message);
return ValidationErrorExitCode;
}
}

/// <summary>
/// Creates an instance of <typeparamref name="TApp"/>, matching <paramref name="args"/>
/// to all attributes on the type, and then invoking a method named "OnExecute" or "OnExecuteAsync" if it exists.
/// See <seealso cref="OptionAttribute" />, <seealso cref="ArgumentAttribute" />,
/// <seealso cref="HelpOptionAttribute"/>, and <seealso cref="VersionOptionAttribute"/>.
/// </summary>
/// <param name="console">The console to use</param>
/// <param name="args">The arguments</param>
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static int Execute<TApp>(IConsole console, params string[] args)
public static int Execute<TApp>(params string[] args)
where TApp : class, new()
{
args = args ?? new string[0];
var context = new DefaultCommandLineContext(console, Directory.GetCurrentDirectory(), args);
return Execute<TApp>(context);
}
=> Execute<TApp>(PhysicalConsole.Singleton, args);

/// <summary>
/// Creates an instance of <typeparamref name="TApp"/>, matching <see cref="CommandLineContext.Arguments"/>
/// Creates an instance of <typeparamref name="TApp"/>, matching <paramref name="args"/>
/// to all attributes on the type, and then invoking a method named "OnExecute" or "OnExecuteAsync" if it exists.
/// See <seealso cref="OptionAttribute" />, <seealso cref="ArgumentAttribute" />,
/// <seealso cref="HelpOptionAttribute"/>, and <seealso cref="VersionOptionAttribute"/>.
/// </summary>
/// <param name="context">The execution context.</param>
/// <param name="console">The console to use</param>
/// <param name="args">The arguments</param>
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static int Execute<TApp>(CommandLineContext context)
public static int Execute<TApp>(IConsole console, params string[] args)
where TApp : class, new()
{
ValidateContextIsNotNull(context);

try
{
using (var bindResult = Bind<TApp>(context))
{
if (bindResult.Command.IsShowingInformation)
{
return HelpExitCode;
}

if (bindResult.ValidationResult != ValidationResult.Success)
{
return HandleValidationError(context, bindResult);
}

var invoker = ExecuteMethodInvoker.Create(bindResult.Target.GetType());
switch (invoker)
{
case AsyncMethodInvoker asyncInvoker:
return asyncInvoker.ExecuteAsync(context, bindResult).GetAwaiter().GetResult();
case SynchronousMethodInvoker syncInvoker:
return syncInvoker.Execute(context, bindResult);
default:
throw new NotImplementedException();
}
}
}
catch (CommandParsingException ex)
{
context.Console.Error.WriteLine(ex.Message);
return ValidationErrorExitCode;
}
args = args ?? new string[0];
var context = new DefaultCommandLineContext(console, Directory.GetCurrentDirectory(), args);
return Execute<TApp>(context);
}

/// <summary>
Expand All @@ -109,8 +110,8 @@ public static int Execute<TApp>(CommandLineContext context)
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static Task<int> ExecuteAsync<TApp>(params string[] args)
where TApp : class, new()
=> ExecuteAsync<TApp>(PhysicalConsole.Singleton, args);
where TApp : class, new()
=> ExecuteAsync<TApp>(PhysicalConsole.Singleton, args);

/// <summary>
/// Creates an instance of <typeparamref name="TApp"/>, matching <paramref name="args"/>
Expand Down Expand Up @@ -141,95 +142,8 @@ public static Task<int> ExecuteAsync<TApp>(IConsole console, params string[] arg
/// <typeparam name="TApp">A type that should be bound to the arguments.</typeparam>
/// <exception cref="InvalidOperationException">Thrown when attributes are incorrectly configured.</exception>
/// <returns>The process exit code</returns>
public static async Task<int> ExecuteAsync<TApp>(CommandLineContext context)
public static Task<int> ExecuteAsync<TApp>(CommandLineContext context)
where TApp : class, new()
{
ValidateContextIsNotNull(context);

try
{
using (var bindResult = Bind<TApp>(context))
{
if (bindResult.Command.IsShowingInformation)
{
return HelpExitCode;
}

if (bindResult.ValidationResult != ValidationResult.Success)
{
return HandleValidationError(context, bindResult);
}

var invoker = ExecuteMethodInvoker.Create(bindResult.Target.GetType());
switch (invoker)
{
case AsyncMethodInvoker asyncInvoker:
return await asyncInvoker.ExecuteAsync(context, bindResult);
case SynchronousMethodInvoker syncInvoker:
return syncInvoker.Execute(context, bindResult);
default:
throw new NotImplementedException();
}
}
}
catch (CommandParsingException ex)
{
context.Console.Error.WriteLine(ex.Message);
return ValidationErrorExitCode;
}
}

private static int HandleValidationError(CommandLineContext context, BindResult bindResult)
{
const BindingFlags MethodFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

var method = bindResult.Target
.GetType()
.GetTypeInfo()
.GetMethod("OnValidationError", MethodFlags);

if (method == null)
{
return bindResult.Command.DefaultValidationErrorHandler(bindResult.ValidationResult);
}

var arguments = ReflectionHelper.BindParameters(method, context, bindResult);
var result = method.Invoke(bindResult.Target, arguments);
if (method.ReturnType == typeof(int))
{
return (int)result;
}

return ValidationErrorExitCode;
}

private static BindResult Bind<TApp>(CommandLineContext context) where TApp : class, new()
{
var applicationBuilder = new ReflectionAppBuilder<TApp>();
return applicationBuilder.Bind(context);
}

private static void ValidateContextIsNotNull(CommandLineContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Arguments == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Arguments));
}

if (context.WorkingDirectory == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.WorkingDirectory));
}

if (context.Console == null)
{
throw new ArgumentNullException(nameof(context) + "." + nameof(context.Console));
}
}
=> Task.FromResult(Execute<TApp>(context));
}
}

0 comments on commit d9cb6d3

Please sign in to comment.