Skip to content

Commit

Permalink
Merge pull request #408 from jonsequitur/decompose-ReflectionBinder
Browse files Browse the repository at this point in the history
replace earlier binding infrastructure
  • Loading branch information
jonsequitur authored Feb 20, 2019
2 parents d9ec688 + c838aa2 commit c9450cb
Show file tree
Hide file tree
Showing 90 changed files with 1,967 additions and 2,668 deletions.
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CommandLine.Binding;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.Tests;
using System.CommandLine.Tests.Binding;
using FluentAssertions;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

namespace System.CommandLine.Tests
namespace System.CommandLine.DragonFruit.Tests
{
public class ConfigureFromMethodTests
{
private object[] _receivedValues;
private readonly TestConsole _testConsole = new TestConsole();
private readonly ITestOutputHelper _output;

public ConfigureFromMethodTests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public async Task Generated_boolean_parameters_will_accept_zero_arguments()
{
var rootCommand = new RootCommand();
rootCommand.ConfigureFromMethod(GetMethodInfo(nameof(Method_taking_bool)), this);
var parser = new CommandLineBuilder()
.ConfigureRootCommandFromMethod(
GetMethodInfo(nameof(Method_taking_bool)), this)
.Build();

await rootCommand.InvokeAsync($"{RootCommand.ExeName} --value", _testConsole);
await parser.InvokeAsync($"{RootCommand.ExeName} --value", _testConsole);

_receivedValues.Should().BeEquivalentTo(true);
}
Expand All @@ -42,21 +42,25 @@ public async Task Generated_boolean_parameters_will_accept_zero_arguments()
[InlineData("--value=false", false)]
public async Task Generated_boolean_parameters_will_accept_one_argument(string commandLine, bool expected)
{
var rootCommand = new RootCommand();
rootCommand.ConfigureFromMethod(GetMethodInfo(nameof(Method_taking_bool)), this);
var parser = new CommandLineBuilder()
.ConfigureRootCommandFromMethod(
GetMethodInfo(nameof(Method_taking_bool)), this)
.Build();

await rootCommand.InvokeAsync(commandLine, _testConsole);
await parser.InvokeAsync(commandLine, _testConsole);

_receivedValues.Should().BeEquivalentTo(expected);
}

[Fact]
public async Task Single_character_parameters_generate_aliases_that_accept_a_single_dash_prefix()
{
var command = new Command("the-command");
command.ConfigureFromMethod(GetMethodInfo(nameof(Method_with_single_letter_parameters)), this);
var parser = new CommandLineBuilder()
.ConfigureRootCommandFromMethod(
GetMethodInfo(nameof(Method_with_single_letter_parameters)), this)
.Build();

await command.InvokeAsync("-x 123 -y 456", _testConsole);
await parser.InvokeAsync("-x 123 -y 456", _testConsole);

_receivedValues.Should()
.BeEquivalentSequenceTo(123, 456);
Expand All @@ -65,36 +69,60 @@ public async Task Single_character_parameters_generate_aliases_that_accept_a_sin
[Fact]
public async Task When_method_returns_void_then_return_code_is_0()
{
var command = new Command("the-command");
command.ConfigureFromMethod(GetMethodInfo(nameof(Method_returning_void)), this);
var parser = new CommandLineBuilder()
.ConfigureRootCommandFromMethod(
GetMethodInfo(nameof(Method_returning_void)), this)
.Build();

var result = await command.InvokeAsync("", _testConsole);
var result = await parser.InvokeAsync("", _testConsole);

result.Should().Be(0);
}

[Fact]
public async Task When_method_returns_int_then_return_code_is_set_to_return_value()
{
var command = new Command("the-command");
command.ConfigureFromMethod(GetMethodInfo(nameof(Method_returning_int)), this);
var parser = new CommandLineBuilder()
.ConfigureRootCommandFromMethod(
GetMethodInfo(nameof(Method_returning_int)), this)
.Build();

var result = await command.InvokeAsync("-i 123", _testConsole);
var result = await parser.InvokeAsync("-i 123", _testConsole);

result.Should().Be(123);
}

[Fact]
public async Task When_method_returns_Task_of_int_then_return_code_is_set_to_return_value()
{
var command = new Command("the-command");
command.ConfigureFromMethod(GetMethodInfo(nameof(Method_returning_Task_of_int)), this);
var parser = new CommandLineBuilder()
.ConfigureRootCommandFromMethod(
GetMethodInfo(nameof(Method_returning_Task_of_int)), this)
.Build();

var result = await command.InvokeAsync("-i 123", _testConsole);
var result = await parser.InvokeAsync("-i 123", _testConsole);

result.Should().Be(123);
}

[Theory]
[InlineData(typeof(IConsole))]
[InlineData(typeof(InvocationContext))]
[InlineData(typeof(BindingContext))]
[InlineData(typeof(ParseResult))]
[InlineData(typeof(CancellationToken))]
public void Options_are_not_built_for_infrastructure_types_exposed_by_method_parameters(Type type)
{
var targetType = typeof(ClassWithMethodHavingParameter<>).MakeGenericType(type);

var handlerMethod = targetType.GetMethod(nameof(ClassWithMethodHavingParameter<int>.Handle));

var options = handlerMethod.BuildOptions();

options.Should()
.NotContain(o => o.Argument.ArgumentType == type);
}

internal void Method_taking_bool(bool value = false)
{
_receivedValues = new object[] { value };
Expand Down
142 changes: 140 additions & 2 deletions src/System.CommandLine.DragonFruit/CommandLine.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.CommandLine.Binding;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.Rendering;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace System.CommandLine.DragonFruit
Expand All @@ -20,7 +23,7 @@ public static class CommandLine
/// <param name="args">The string arguments.</param>
/// <returns>The exit code.</returns>
public static async Task<int> ExecuteAssemblyAsync(
Assembly entryAssembly,
Assembly entryAssembly,
string[] args)
{
if (entryAssembly == null)
Expand All @@ -44,7 +47,7 @@ public static async Task<int> InvokeMethodAsync(
IConsole console = null)
{
var builder = new CommandLineBuilder()
.ConfigureFromMethod(method, target)
.ConfigureRootCommandFromMethod(method, target)
.ConfigureHelpFromXmlComments(method)
.UseDefaults()
.UseAnsiTerminalWhenAvailable();
Expand All @@ -54,10 +57,82 @@ public static async Task<int> InvokeMethodAsync(
return await parser.InvokeAsync(args, console);
}

public static CommandLineBuilder ConfigureRootCommandFromMethod(
this CommandLineBuilder builder,
MethodInfo method,
object target = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (method == null)
{
throw new ArgumentNullException(nameof(method));
}

builder.Command.ConfigureFromMethod(method, target);

if (target != null)
{
builder.UseMiddleware(
async (context, next) =>
{
context.BindingContext
.AddService(
target.GetType(),
() => target);
await next(context);
});
}

return builder;
}

internal static void ConfigureFromMethod(
this Command command,
MethodInfo method,
object target = null) =>
command.ConfigureFromMethod(method, () => target);

public static void ConfigureFromMethod(
this Command command,
MethodInfo method,
Func<object> target)
{
if (command == null)
{
throw new ArgumentNullException(nameof(command));
}

if (method == null)
{
throw new ArgumentNullException(nameof(method));
}

foreach (var option in method.BuildOptions())
{
command.AddOption(option);
}

command.Handler = CommandHandler.Create(method);
}

public static CommandLineBuilder ConfigureHelpFromXmlComments(
this CommandLineBuilder builder,
MethodInfo method)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (method == null)
{
throw new ArgumentNullException(nameof(method));
}

Assembly assembly = method.DeclaringType.Assembly;
string docFilePath = Path.Combine(
Path.GetDirectoryName(assembly.Location),
Expand Down Expand Up @@ -90,5 +165,68 @@ public static CommandLineBuilder ConfigureHelpFromXmlComments(

return builder;
}

public static string BuildAlias(this IValueDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}

return BuildAlias(descriptor.Name);
}

internal static string BuildAlias(string parameterName)
{
if (String.IsNullOrWhiteSpace(parameterName))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(parameterName));
}

return parameterName.Length > 1
? $"--{parameterName.ToKebabCase()}"
: $"-{parameterName.ToLowerInvariant()}";
}

public static IEnumerable<Option> BuildOptions(this MethodInfo type)
{
var descriptor = HandlerDescriptor.FromMethodInfo(type);

var omittedTypes = new[]
{
typeof(IConsole),
typeof(InvocationContext),
typeof(BindingContext),
typeof(ParseResult),
typeof(CancellationToken),
};

foreach (var option in descriptor.ParameterDescriptors
.Where(d => !omittedTypes.Contains (d.Type))
.Select(p => p.BuildOption()))
{
yield return option;
}
}

public static Option BuildOption(this ParameterDescriptor parameter)
{
var argument = new Argument
{
ArgumentType = parameter.Type
};

if (parameter.HasDefaultValue)
{
argument.SetDefaultValue(parameter.GetDefaultValue);
}

var option = new Option(
parameter.BuildAlias(),
parameter.Name,
argument);

return option;
}
}
}
15 changes: 12 additions & 3 deletions src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ public class Program
{
static async Task Main(string[] args)
{
var commandLine = new CommandLineBuilder()
.UseHelp()
var rootCommand = new RootCommand
{
new Option("--apple", argument: new Argument<string>()),
new Option("--banana", argument: new Argument<string>()),
new Option("--cherry", argument: new Argument<string>()),
new Option("--durian", argument: new Argument<string>())
};

rootCommand.Handler = CommandHandler.Create(typeof(Program).GetMethod(nameof(Run)));

var commandLine = new CommandLineBuilder(rootCommand)
.UseHelp()
.UseSuggestDirective()
.UseExceptionHandler()
.RegisterWithDotnetSuggest()
.ConfigureFromMethod(typeof(Program).GetMethod(nameof(Run)))
.Build();

await commandLine.InvokeAsync(args);
Expand Down
13 changes: 10 additions & 3 deletions src/System.CommandLine.Tests/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ public static AndConstraint<GenericCollectionAssertions<T>> BeEquivalentSequence
var actualValues = assertions.Subject.ToArray();

actualValues
.Select(a => a.GetType())
.Select(a => a?.GetType())
.Should()
.BeEquivalentTo(expectedValues.Select(e => e.GetType()));
.BeEquivalentTo(expectedValues.Select(e => e?.GetType()));

using (new AssertionScope())
{
foreach (var tuple in actualValues
.Zip(expectedValues, (actual, expected) => (actual, expected))
.Where(t => t.expected.GetType().GetProperties().Any()))
.Where(t => t.expected == null || t.expected.GetType().GetProperties().Any()))

{
tuple.actual
Expand All @@ -36,5 +36,12 @@ public static AndConstraint<GenericCollectionAssertions<T>> BeEquivalentSequence

return new AndConstraint<GenericCollectionAssertions<T>>(assertions);
}

public static AndConstraint<StringCollectionAssertions> BeEquivalentSequenceTo(
this StringCollectionAssertions assertions,
params string[] expectedValues)
{
return assertions.BeEquivalentTo(expectedValues, c => c.WithStrictOrderingFor(s => s));
}
}
}
2 changes: 1 addition & 1 deletion src/System.CommandLine.Tests/Binding/CommandExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static BindingContext CreateBindingContext(

var parseResult = parser.Parse(commandLine);

return new BindingContext(parseResult, parser);
return new BindingContext(parseResult);
}
}
}
Loading

0 comments on commit c9450cb

Please sign in to comment.