From c854f5fb8d8fdbfe7fb0f6b04624e5318ef5af00 Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Mon, 16 Mar 2020 14:48:48 +0200 Subject: [PATCH] Throw errors on unrecognized input Closes #38 Closes #24 --- CliFx.Tests/ArgumentBindingSpecs.Commands.cs | 12 ++++++++ CliFx.Tests/ArgumentBindingSpecs.cs | 32 ++++++++++++++++++++ CliFx/Domain/CommandSchema.cs | 28 +++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/CliFx.Tests/ArgumentBindingSpecs.Commands.cs b/CliFx.Tests/ArgumentBindingSpecs.Commands.cs index 23e7921..ec2d0de 100644 --- a/CliFx.Tests/ArgumentBindingSpecs.Commands.cs +++ b/CliFx.Tests/ArgumentBindingSpecs.Commands.cs @@ -166,5 +166,17 @@ namespace CliFx.Tests public ValueTask ExecuteAsync(IConsole console) => default; } + + [Command] + private class NoParameterCommand : ICommand + { + [CommandOption(nameof(OptionA))] + public string? OptionA { get; set; } + + [CommandOption(nameof(OptionB))] + public string? OptionB { get; set; } + + public ValueTask ExecuteAsync(IConsole console) => default; + } } } \ No newline at end of file diff --git a/CliFx.Tests/ArgumentBindingSpecs.cs b/CliFx.Tests/ArgumentBindingSpecs.cs index a1c13a4..7a604b9 100644 --- a/CliFx.Tests/ArgumentBindingSpecs.cs +++ b/CliFx.Tests/ArgumentBindingSpecs.cs @@ -1018,5 +1018,37 @@ namespace CliFx.Tests // Act & assert Assert.Throws(() => schema.InitializeEntryPoint(input)); } + + [Fact] + public void All_provided_option_arguments_must_be_bound_to_corresponding_properties() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption("not-a-real-option", "boom") + .AddOption("fake-option", "poof") + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } + + [Fact] + public void All_provided_parameter_arguments_must_be_bound_to_corresponding_properties() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(NoParameterCommand)}); + + var input = new CommandLineInputBuilder() + .AddUnboundArgument("boom") + .AddUnboundArgument("poof") + .AddOption(nameof(NoParameterCommand.OptionA), "foo") + .AddOption(nameof(NoParameterCommand.OptionB), "bar") + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } } } \ No newline at end of file diff --git a/CliFx/Domain/CommandSchema.cs b/CliFx/Domain/CommandSchema.cs index b8b6a7e..c0129cf 100644 --- a/CliFx/Domain/CommandSchema.cs +++ b/CliFx/Domain/CommandSchema.cs @@ -42,6 +42,9 @@ namespace CliFx.Domain private void InjectParameters(ICommand command, IReadOnlyList parameterInputs) { + // All inputs must be bound + var remainingParameterInputs = parameterInputs.ToList(); + // Scalar parameters var scalarParameters = Parameters .OrderBy(p => p.Order) @@ -57,6 +60,7 @@ namespace CliFx.Domain : throw new CliFxException($"Missing value for parameter <{scalarParameter.DisplayName}>."); scalarParameter.Inject(command, scalarParameterInput); + remainingParameterInputs.Remove(scalarParameterInput); } // Non-scalar parameter (only one is allowed) @@ -68,6 +72,16 @@ namespace CliFx.Domain { var nonScalarParameterInputs = parameterInputs.Skip(scalarParameters.Length).ToArray(); nonScalarParameter.Inject(command, nonScalarParameterInputs); + remainingParameterInputs.Clear(); + } + + // Ensure all inputs were bound + if (remainingParameterInputs.Any()) + { + throw new CliFxException(new StringBuilder() + .AppendLine("Unrecognized parameters provided:") + .AppendBulletList(remainingParameterInputs) + .ToString()); } } @@ -76,6 +90,9 @@ namespace CliFx.Domain IReadOnlyList optionInputs, IReadOnlyDictionary environmentVariables) { + // All inputs must be bound + var remainingOptionInputs = optionInputs.ToList(); + // Keep track of required options so that we can raise an error if any of them are not set var unsetRequiredOptions = Options.Where(o => o.IsRequired).ToList(); @@ -103,10 +120,12 @@ namespace CliFx.Domain if (option != null) { option.Inject(command, optionInput.Values); + remainingOptionInputs.Remove(optionInput); unsetRequiredOptions.Remove(option); } } + // Ensure all required options were set if (unsetRequiredOptions.Any()) { throw new CliFxException(new StringBuilder() @@ -114,6 +133,15 @@ namespace CliFx.Domain .AppendBulletList(unsetRequiredOptions.Select(o => o.DisplayName)) .ToString()); } + + // Ensure all inputs were bound + if (remainingOptionInputs.Any()) + { + throw new CliFxException(new StringBuilder() + .AppendLine("Unrecognized options provided:") + .AppendBulletList(remainingOptionInputs.Select(o => o.Alias)) + .ToString()); + } } public ICommand CreateInstance(