Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions src/SystemCommandLine.Extensions.Tests/CommandBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class CommandBuilderTests
{
private class TestRootCommand : RootCommand, IUseCommandBuilder<TestRootCommand>
{
public static TestRootCommand CommandFactory(IServiceProvider serviceProvider, ArgumentMapperRegistration mapperRegistration)
public static TestRootCommand CommandFactory(IServiceProvider serviceProvider, SymbolMapperRegistration mapperRegistration)
{
return new TestRootCommand() {
serviceProvider.GetRequiredService<TestCommand>()
Expand All @@ -20,7 +20,7 @@ public static TestRootCommand CommandFactory(IServiceProvider serviceProvider, A

private class TestCommand : Command, IUseCommandBuilder<TestCommand>
{
public TestCommand(ArgumentMapperRegistration mapperRegistration) : base("test", "A test command")
public TestCommand(SymbolMapperRegistration mapperRegistration) : base("test", "A test command")
{
this.UseCommandBuilder().WithMapping<TestCommandOptions>(mapperRegistration)
.NewOption(x => x.Name).Configure(o =>
Expand All @@ -32,6 +32,10 @@ public TestCommand(ArgumentMapperRegistration mapperRegistration) : base("test",
{
o.Description = "Name with property default";
}).AddToCommand()
.NewArgument(x => x.NameArgument).Configure(o =>
{
o.Description = "Name argument";
}).AddToCommand()
.NewOption(x => x.Count).Configure(o =>
{
o.Description = "Count for the test";
Expand All @@ -41,14 +45,20 @@ public TestCommand(ArgumentMapperRegistration mapperRegistration) : base("test",
{
o.Description = "An option that is not mapped to the options class";
o.DefaultValueFactory = _ => "!";
}).AddToCommand()
.NewArgument<string>("NotMappedArgument").Configure(o =>
{
o.Description = "An argument that is not mapped to the options class";
o.DefaultValueFactory = _ => "!";
}).AddToCommand();
}
public static TestCommand CommandFactory(IServiceProvider serviceProvider, ArgumentMapperRegistration mapperRegistration) => new(mapperRegistration);
public static TestCommand CommandFactory(IServiceProvider serviceProvider, SymbolMapperRegistration mapperRegistration) => new(mapperRegistration);

public class TestCommandOptions
{
public required string Name { get; set; }
public string NameWithDefault { get; set; } = "PropertyDefault";
public required string NameArgument { get; set; }
public int Count { get; set; }
}
public class TestHandler(IOptions<TestCommandOptions> options) : SynchronousCommandLineAction
Expand All @@ -70,9 +80,10 @@ public override async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
private static int Handler(string handlerType, ParseResult parseResult, IOptions<TestCommandOptions> options)
{
string notMappedOption = parseResult.GetRequiredValue<string>(NameFormatExtensions.ToKebabCase("--", nameof(notMappedOption)));
string notMappedArgument = parseResult.GetRequiredValue<string>(NameFormatExtensions.ToKebabCase(nameof(notMappedArgument)));

parseResult.InvocationConfiguration
.Output.WriteLine($"Running {handlerType} test '{options.Value.Name}' '{options.Value.NameWithDefault}' {options.Value.Count} times {notMappedOption}");
.Output.WriteLine($"Running {handlerType} test '{options.Value.Name}' '{options.Value.NameWithDefault}' '{options.Value.NameArgument}' {options.Value.Count} times {notMappedOption} {notMappedArgument}");

parseResult.InvocationConfiguration
.Error.WriteLine($"Error message");
Expand Down Expand Up @@ -102,7 +113,7 @@ private static ServiceProvider GetServiceProviderWithHandler(string args)
[Fact]
public void Should_run_synchronous_handler()
{
using var serviceProvider = GetServiceProviderWithHandler("test --name MyTest --count 5 --not-mapped-option !!!");
using var serviceProvider = GetServiceProviderWithHandler("test --name MyTest --count 5 --not-mapped-option !!! foo bar");
ParseResult parserResult = serviceProvider.GetRequiredService<ParseResult>();

InvocationConfiguration configuration = new()
Expand All @@ -113,13 +124,13 @@ public void Should_run_synchronous_handler()
int result = parserResult.Invoke(configuration);
Assert.Matches(@"Error message", configuration.Error.ToString());
Assert.Equal(42, result);
Assert.Matches(@"Running sync test 'MyTest' 'PropertyDefault' 5 times !!!", configuration.Output.ToString());
Assert.Matches(@"Running sync test 'MyTest' 'PropertyDefault' 'foo' 5 times !!! bar", configuration.Output.ToString());
}

[Fact]
public async Task Should_run_asynchronous_handler()
{
using ServiceProvider serviceProvider = GetServiceProviderWithAsyncHandler("test --name MyTest --count 5 --not-mapped-option !!!");
using ServiceProvider serviceProvider = GetServiceProviderWithAsyncHandler("test --name MyTest --count 5 --not-mapped-option !!! foo bar");

ParseResult parserResult = serviceProvider.GetRequiredService<ParseResult>();

Expand All @@ -132,13 +143,13 @@ public async Task Should_run_asynchronous_handler()

Assert.Matches(@"Error message", configuration.Error.ToString());
Assert.Equal(42, result);
Assert.Matches(@"Running async test 'MyTest' 'PropertyDefault' 5 times !!!", configuration.Output.ToString());
Assert.Matches(@"Running async test 'MyTest' 'PropertyDefault' 'foo' 5 times !!! bar", configuration.Output.ToString());
}

[Fact]
public void Should_allow_option_and_property_default_values()
{
using ServiceProvider serviceProvider = GetServiceProviderWithHandler("test");
using ServiceProvider serviceProvider = GetServiceProviderWithHandler("test foo");

ParseResult parserResult = serviceProvider.GetRequiredService<ParseResult>();

Expand All @@ -150,6 +161,6 @@ public void Should_allow_option_and_property_default_values()
int result = parserResult.Invoke(configuration);

Assert.Equal(42, result);
Assert.Matches(@"Running sync test 'OptionDefault' 'PropertyDefault' 1 times !", configuration.Output.ToString());
Assert.Matches(@"Running sync test 'OptionDefault' 'PropertyDefault' 'foo' 1 times ! !", configuration.Output.ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void GetPropertyName_returns_property_name()
public void CreateArgumentMapper_sets_reference_type_property_value()
{
// Arrange
Action<Holder, string?> mapper = ExpressionExtensions.CreateArgumentValueMapper<Holder, string?>(h => h.Name);
Action<Holder, string?> mapper = ExpressionExtensions.CreateSymbolValueMapper<Holder, string?>(h => h.Name);
Holder holder = new();

// Act
Expand All @@ -39,7 +39,7 @@ public void CreateArgumentMapper_sets_reference_type_property_value()
public void CreateArgumentMapper_does_not_set_null_value()
{
// Arrange
Action<Holder, string?> mapper = ExpressionExtensions.CreateArgumentValueMapper<Holder, string?>(h => h.Name);
Action<Holder, string?> mapper = ExpressionExtensions.CreateSymbolValueMapper<Holder, string?>(h => h.Name);
Holder holder = new();

// Act
Expand All @@ -53,7 +53,7 @@ public void CreateArgumentMapper_does_not_set_null_value()
public void CreateArgumentMapper_sets_value_type_property_value()
{
// Arrange
Action<Holder, int> mapper = ExpressionExtensions.CreateArgumentValueMapper<Holder, int>(h => h.Number);
Action<Holder, int> mapper = ExpressionExtensions.CreateSymbolValueMapper<Holder, int>(h => h.Number);
Holder holder = new();

// Act
Expand All @@ -68,7 +68,7 @@ public void CreateArgumentMapper_throws_when_setter_is_not_public()
{
// Act
ArgumentException ex = Assert.Throws<ArgumentException>(() =>
ExpressionExtensions.CreateArgumentValueMapper<Holder, string>(h => h.WithPrivateSetter));
ExpressionExtensions.CreateSymbolValueMapper<Holder, string>(h => h.WithPrivateSetter));

// Assert
Assert.Equal("Property Holder.WithPrivateSetter has no accessible setter.", ex.Message);
Expand All @@ -79,7 +79,7 @@ public void CreateArgumentMapper_throws_when_property_has_no_setter()
{
// Act
ArgumentException ex = Assert.Throws<ArgumentException>(() =>
ExpressionExtensions.CreateArgumentValueMapper<Holder, string>(h => h.ReadOnly));
ExpressionExtensions.CreateSymbolValueMapper<Holder, string>(h => h.ReadOnly));

// Assert
Assert.Equal("Property Holder.ReadOnly has no accessible setter.", ex.Message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="1.8.4" />
<PackageReference Include="xunit.v3" Version="3.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

namespace SystemCommandLine.Extensions.Builders;

internal class CommandArgumentBuilder<TCommand, TOption>(ICommandBuilder<TCommand> commandBuilder, TCommand command, string name) : ICommandArgumentBuilder<TCommand, TOption> where TCommand : Command, IUseCommandBuilder<TCommand>
internal class CommandArgumentBuilder<TCommand, TOption>(ICommandBuilder<TCommand> commandBuilder, TCommand command, string name) :
ICommandArgumentBuilder<TCommand, TOption> where TCommand : Command, IUseCommandBuilder<TCommand>
{
protected readonly Option<TOption> option = new(NameFormatExtensions.ToKebabCase("--", name));
protected readonly Argument<TOption> argument = new(NameFormatExtensions.ToKebabCase(name));

public virtual ICommandArgumentBuilder<TCommand, TOption> Configure(Action<Option<TOption>> value)
public virtual ICommandArgumentBuilder<TCommand, TOption> Configure(Action<Argument<TOption>> value)
{
value.Invoke(option);
value.Invoke(argument);
return this;
}

public virtual ICommandBuilder<TCommand> AddToCommand()
{
command.Add(option);
command.Add(argument);
return commandBuilder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,26 @@

namespace SystemCommandLine.Extensions.Builders;

internal class CommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption>(TCommand command, ICommandBuilderWithMapping<TCommand, TOptionHolder> commandHandlerBuilder, Expression<Func<TOptionHolder, TOption>> propertyExpression, ArgumentMapperRegistration mapperRegistration) : ICommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption> where TCommand : Command, IUseCommandBuilder<TCommand>
internal class CommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption>(
TCommand command, ICommandBuilderWithMapping<TCommand, TOptionHolder> commandHandlerBuilder, Expression<Func<TOptionHolder, TOption>> propertyExpression, SymbolMapperRegistration mapperRegistration) :
CommandSymbolBuilderWithMapping<TOptionHolder, TOption>(propertyExpression, mapperRegistration),
ICommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption> where TCommand : Command, IUseCommandBuilder<TCommand>
where TOptionHolder : class
{
private readonly Option<TOption> option = new(ToKebabCase(propertyExpression.GetPropertyName()));
private readonly Argument<TOption> argument = new(NameFormatExtensions.ToKebabCase(propertyExpression.GetPropertyName()));

private static string ToKebabCase(string optionName)
public ICommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption> Configure(Action<Argument<TOption>> value)
{
return NameFormatExtensions.ToKebabCase("--", optionName);
}
public ICommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption> Configure(Action<Option<TOption>> value)
{
value.Invoke(option);
value.Invoke(argument);
return this;
}

public ICommandBuilderWithMapping<TCommand, TOptionHolder> AddToCommand()
{
command.Add(option);
command.Add(argument);

RegisterArgumentMapper();
RegisterSymbolMapper(argument);

return commandHandlerBuilder;
}

private void RegisterArgumentMapper()
{
Action<TOptionHolder, TOption?> argumentValueMapper = propertyExpression.CreateArgumentValueMapper();

void argumentMapper(ParseResult parsedResult, object options)
{
argumentValueMapper((TOptionHolder)options, parsedResult.GetValue<TOption>(option.Name));
}

mapperRegistration(argumentMapper);
}
}
}
9 changes: 7 additions & 2 deletions src/SystemCommandLine.Extensions/Builders/CommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ namespace SystemCommandLine.Extensions.Builders;

internal class CommandBuilder<TCommand>(TCommand command) : ICommandBuilder<TCommand> where TCommand : Command, IUseCommandBuilder<TCommand>
{
public ICommandArgumentBuilder<TCommand, TOption> NewOption<TOption>(string name)
public ICommandArgumentBuilder<TCommand, TOption> NewArgument<TOption>(string name)
{
return new CommandArgumentBuilder<TCommand, TOption>(this, command, name);
}

public ICommandBuilderWithMapping<TCommand, TOptionHolder> WithMapping<TOptionHolder>(ArgumentMapperRegistration mapperRegistration)
public ICommandOptionBuilder<TCommand, TOption> NewOption<TOption>(string name)
{
return new CommandOptionBuilder<TCommand, TOption>(this, command, name);
}

public ICommandBuilderWithMapping<TCommand, TOptionHolder> WithMapping<TOptionHolder>(SymbolMapperRegistration mapperRegistration)
where TOptionHolder : class
{
return new CommandBuilderWithMapping<TCommand, TOptionHolder>(this, command, mapperRegistration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@

namespace SystemCommandLine.Extensions.Builders;

internal class CommandBuilderWithMapping<TCommand, TOptionHolder>(ICommandBuilder<TCommand> commandBuilder, TCommand command, ArgumentMapperRegistration mapperRegistration) : ICommandBuilderWithMapping<TCommand, TOptionHolder> where TCommand : Command, IUseCommandBuilder<TCommand>
internal class CommandBuilderWithMapping<TCommand, TOptionHolder>(ICommandBuilder<TCommand> commandBuilder, TCommand command, SymbolMapperRegistration mapperRegistration) :
ICommandBuilderWithMapping<TCommand, TOptionHolder> where TCommand : Command, IUseCommandBuilder<TCommand>
where TOptionHolder : class
{
public ICommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption> NewOption<TOption>(Expression<Func<TOptionHolder, TOption>> propertyExpression)
public ICommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption> NewArgument<TOption>(Expression<Func<TOptionHolder, TOption>> propertyExpression)
{
return new CommandArgumentBuilderWithMapping<TCommand, TOptionHolder, TOption>(command, this, propertyExpression, mapperRegistration);
}

public ICommandOptionBuilderWithMapping<TCommand, TOptionHolder, TOption> NewOption<TOption>(Expression<Func<TOptionHolder, TOption>> propertyExpression)
{
return new CommandOptionBuilderWithMapping<TCommand, TOptionHolder, TOption>(command, this, propertyExpression, mapperRegistration);
}

public ICommandBuilder<TCommand> CommandBuilder() => commandBuilder;
}
21 changes: 21 additions & 0 deletions src/SystemCommandLine.Extensions/Builders/CommandOptionBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.CommandLine;

namespace SystemCommandLine.Extensions.Builders;

internal class CommandOptionBuilder<TCommand, TOption>(ICommandBuilder<TCommand> commandBuilder, TCommand command, string name) :
ICommandOptionBuilder<TCommand, TOption> where TCommand : Command, IUseCommandBuilder<TCommand>
{
protected readonly Option<TOption> option = new(NameFormatExtensions.ToKebabCase("--", name));

public virtual ICommandOptionBuilder<TCommand, TOption> Configure(Action<Option<TOption>> value)
{
value.Invoke(option);
return this;
}

public virtual ICommandBuilder<TCommand> AddToCommand()
{
command.Add(option);
return commandBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.CommandLine;
using System.Linq.Expressions;

namespace SystemCommandLine.Extensions.Builders;

internal class CommandOptionBuilderWithMapping<TCommand, TOptionHolder, TOption>(
TCommand command, ICommandBuilderWithMapping<TCommand, TOptionHolder> commandHandlerBuilder, Expression<Func<TOptionHolder, TOption>> propertyExpression, SymbolMapperRegistration mapperRegistration) :
CommandSymbolBuilderWithMapping<TOptionHolder, TOption>(propertyExpression, mapperRegistration),
ICommandOptionBuilderWithMapping<TCommand, TOptionHolder, TOption> where TCommand : Command, IUseCommandBuilder<TCommand>
where TOptionHolder : class
{
private readonly Option<TOption> option = new(NameFormatExtensions.ToKebabCase("--", propertyExpression.GetPropertyName()));

public ICommandOptionBuilderWithMapping<TCommand, TOptionHolder, TOption> Configure(Action<Option<TOption>> value)
{
value.Invoke(option);
return this;
}

public ICommandBuilderWithMapping<TCommand, TOptionHolder> AddToCommand()
{
command.Add(option);

RegisterSymbolMapper(option);

return commandHandlerBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.CommandLine;
using System.Linq.Expressions;

namespace SystemCommandLine.Extensions.Builders;

internal abstract class CommandSymbolBuilderWithMapping<TOptionHolder, TOption>(Expression<Func<TOptionHolder, TOption>> propertyExpression, SymbolMapperRegistration mapperRegistration) where TOptionHolder : class
{
protected void RegisterSymbolMapper(Symbol symbol)
{
Action<TOptionHolder, TOption?> symbolValueMapper = propertyExpression.CreateSymbolValueMapper();

void symbolMapper(ParseResult parsedResult, object options)
{
if (options is TOptionHolder typedOptions)
symbolValueMapper(typedOptions, parsedResult.GetValue<TOption>(symbol.Name));
}

mapperRegistration(symbolMapper);
}
}
8 changes: 0 additions & 8 deletions src/SystemCommandLine.Extensions/CommandArgumentMapper.cs

This file was deleted.

8 changes: 8 additions & 0 deletions src/SystemCommandLine.Extensions/CommandSymbolMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.CommandLine;

namespace SystemCommandLine.Extensions;

public delegate void SymbolMapper(ParseResult parseResult, object options);
public delegate void SymbolMapperRegistration(SymbolMapper mapper);

internal class CommandSymbolMapper<TCommand> : List<SymbolMapper> { }
Loading