diff --git a/CliFx.Tests/ArgumentBindingSpecs.Commands.cs b/CliFx.Tests/ArgumentBindingSpecs.Commands.cs index c50a827..55e25d5 100644 --- a/CliFx.Tests/ArgumentBindingSpecs.Commands.cs +++ b/CliFx.Tests/ArgumentBindingSpecs.Commands.cs @@ -134,11 +134,17 @@ namespace CliFx.Tests [Command] private class RequiredOptionCommand : ICommand { - [CommandOption(nameof(OptionA))] - public string? OptionA { get; set; } + [CommandOption(nameof(Option), IsRequired = true)] + public string? Option { get; set; } - [CommandOption(nameof(OptionB), IsRequired = true)] - public string? OptionB { get; set; } + public ValueTask ExecuteAsync(IConsole console) => default; + } + + [Command] + private class RequiredArrayOptionCommand : ICommand + { + [CommandOption(nameof(Option), IsRequired = true)] + public IReadOnlyList? Option { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } diff --git a/CliFx.Tests/ArgumentBindingSpecs.cs b/CliFx.Tests/ArgumentBindingSpecs.cs index 14cbfc4..e014164 100644 --- a/CliFx.Tests/ArgumentBindingSpecs.cs +++ b/CliFx.Tests/ArgumentBindingSpecs.cs @@ -825,113 +825,6 @@ namespace CliFx.Tests }); } - [Fact] - public void Property_annotated_as_an_option_can_be_bound_from_multiple_values_even_if_the_inputs_use_mixed_naming() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption("option", "foo") - .AddOption("o", "bar") - .AddOption("option", "baz") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new ArrayOptionCommand - { - Option = new[] {"foo", "bar", "baz"} - }); - } - - [Fact] - public void Property_annotated_as_a_required_option_must_always_be_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(RequiredOptionCommand.OptionA), "foo") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_a_required_option_must_always_be_bound_to_some_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(RequiredOptionCommand.OptionB)) - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order() - { - // Arrange - var input = new CommandInputBuilder() - .AddParameter("foo") - .AddParameter("bar") - .AddParameter("hello") - .AddParameter("world") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new ParametersCommand - { - ParameterA = "foo", - ParameterB = "bar", - ParameterC = new[] {"hello", "world"} - }); - } - - [Fact] - public void Scalar_properties_annotated_as_parameter_requires_input() - { - // Arrange - var input = new CommandInputBuilder() - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void NonScalar_properties_annotated_as_parameter_requires_input() - { - // Arrange - var input = new CommandInputBuilder() - .AddParameter("foo") - .AddParameter("bar") - .Build(); - - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_parameter_must_always_be_bound_to_some_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddParameter("foo") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - [Fact] public void Property_of_custom_type_that_implements_IEnumerable_can_only_be_bound_if_that_type_has_a_constructor_accepting_an_array() { @@ -984,6 +877,114 @@ namespace CliFx.Tests _output.WriteLine(ex.Message); } + [Fact] + public void Property_annotated_as_an_option_can_be_bound_from_multiple_values_even_if_the_inputs_use_mixed_naming() + { + // Arrange + var input = new CommandInputBuilder() + .AddOption("option", "foo") + .AddOption("o", "bar") + .AddOption("option", "baz") + .Build(); + + // Act + var instance = CommandHelper.ResolveCommand(input); + + // Assert + instance.Should().BeEquivalentTo(new ArrayOptionCommand + { + Option = new[] {"foo", "bar", "baz"} + }); + } + + [Fact] + public void Property_annotated_as_a_required_option_must_always_be_set() + { + // Arrange + var input = new CommandInputBuilder() + .Build(); + + // Act & assert + var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); + _output.WriteLine(ex.Message); + } + + [Fact] + public void Property_annotated_as_a_required_option_must_always_be_bound_to_some_value() + { + // Arrange + var input = new CommandInputBuilder() + .AddOption(nameof(RequiredOptionCommand.Option)) + .Build(); + + // Act & assert + var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); + _output.WriteLine(ex.Message); + } + + [Fact] + public void Property_annotated_as_a_required_option_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values() + { + // Arrange + var input = new CommandInputBuilder() + .AddOption(nameof(RequiredOptionCommand.Option)) + .Build(); + + // Act & assert + var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); + _output.WriteLine(ex.Message); + } + + [Fact] + public void Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order() + { + // Arrange + var input = new CommandInputBuilder() + .AddParameter("foo") + .AddParameter("bar") + .AddParameter("hello") + .AddParameter("world") + .Build(); + + // Act + var instance = CommandHelper.ResolveCommand(input); + + // Assert + instance.Should().BeEquivalentTo(new ParametersCommand + { + ParameterA = "foo", + ParameterB = "bar", + ParameterC = new[] {"hello", "world"} + }); + } + + [Fact] + public void Property_annotated_as_parameter_must_always_be_bound_to_some_value() + { + // Arrange + var input = new CommandInputBuilder() + .AddParameter("foo") + .Build(); + + // Act & assert + var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); + _output.WriteLine(ex.Message); + } + + [Fact] + public void Property_annotated_as_parameter_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values() + { + // Arrange + var input = new CommandInputBuilder() + .AddParameter("foo") + .AddParameter("bar") + .Build(); + + // Act & assert + var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); + _output.WriteLine(ex.Message); + } + [Fact] public void All_provided_option_arguments_must_be_bound_to_corresponding_properties() { diff --git a/CliFx/Domain/CommandSchema.cs b/CliFx/Domain/CommandSchema.cs index 73300ec..e5c0041 100644 --- a/CliFx/Domain/CommandSchema.cs +++ b/CliFx/Domain/CommandSchema.cs @@ -103,15 +103,16 @@ namespace CliFx.Domain if (nonScalarParameter != null) { - // Verify that we have at least one value - if(!parameterInputs.Skip(scalarParameters.Length).Any()) - throw CliFxException.ParameterNotSet(nonScalarParameter); - var nonScalarValues = parameterInputs .Skip(scalarParameters.Length) .Select(p => p.Value) .ToArray(); + // Parameters are required by default and so a non-scalar parameter must + // be bound to at least one value + if(!nonScalarValues.Any()) + throw CliFxException.ParameterNotSet(nonScalarParameter); + nonScalarParameter.BindOn(instance, nonScalarValues); remainingParameterInputs.Clear(); }