From 66f9b1a256accd992c4a52a7b35e03a79bc0a0ed Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Mon, 19 Aug 2019 01:15:10 +0300 Subject: [PATCH] Rework CommandSchemaResolver and move validation there --- CliFx/CliApplication.cs | 120 ++---------------- .../InvalidCommandSchemaException.cs | 26 ++++ CliFx/Models/CommandOptionSchema.cs | 4 +- CliFx/Models/CommandSchema.cs | 8 +- CliFx/Models/Extensions.cs | 4 +- CliFx/Services/CommandSchemaResolver.cs | 90 ++++++++++++- CliFx/Services/Extensions.cs | 10 +- CliFx/Services/HelpTextRenderer.cs | 4 +- CliFx/Services/ICommandSchemaResolver.cs | 5 +- 9 files changed, 145 insertions(+), 126 deletions(-) create mode 100644 CliFx/Exceptions/InvalidCommandSchemaException.cs diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 2d9a68b..2b563eb 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Internal; using CliFx.Models; @@ -13,7 +12,7 @@ namespace CliFx /// /// Default implementation of . /// - public partial class CliApplication : ICliApplication + public class CliApplication : ICliApplication { private readonly ApplicationMetadata _metadata; private readonly ApplicationConfiguration _configuration; @@ -43,85 +42,6 @@ namespace CliFx _helpTextRenderer = helpTextRenderer.GuardNotNull(nameof(helpTextRenderer)); } - private IReadOnlyList GetAvailableCommandSchemasValidationErrors(IReadOnlyList availableCommandSchemas) - { - var result = new List(); - - // Fail if there are no commands defined - if (!availableCommandSchemas.Any()) - { - result.Add("There are no commands defined."); - } - - // Fail if there are commands that don't implement ICommand - var nonImplementedCommandNames = availableCommandSchemas - .Where(c => !c.Type.Implements(typeof(ICommand))) - .Select(c => c.Name) - .Distinct() - .ToArray(); - - foreach (var commandName in nonImplementedCommandNames) - { - result.Add(!commandName.IsNullOrWhiteSpace() - ? $"Command [{commandName}] doesn't implement ICommand." - : "Default command doesn't implement ICommand."); - } - - // Fail if there are multiple commands with the same name - var nonUniqueCommandNames = availableCommandSchemas - .Select(c => c.Name) - .GroupBy(i => i, StringComparer.OrdinalIgnoreCase) - .Where(g => g.Count() >= 2) - .SelectMany(g => g) - .Distinct() - .ToArray(); - - foreach (var commandName in nonUniqueCommandNames) - { - result.Add(!commandName.IsNullOrWhiteSpace() - ? $"There are multiple commands defined with name [{commandName}]." - : "There are multiple default commands defined."); - } - - // Fail if there are multiple options with the same name inside the same command - foreach (var commandSchema in availableCommandSchemas) - { - var nonUniqueOptionNames = commandSchema.Options - .Where(o => !o.Name.IsNullOrWhiteSpace()) - .Select(o => o.Name) - .GroupBy(i => i, StringComparer.OrdinalIgnoreCase) - .Where(g => g.Count() >= 2) - .SelectMany(g => g) - .Distinct() - .ToArray(); - - foreach (var optionName in nonUniqueOptionNames) - { - result.Add(!commandSchema.Name.IsNullOrWhiteSpace() - ? $"There are multiple options defined with name [{optionName}] on command [{commandSchema.Name}]." - : $"There are multiple options defined with name [{optionName}] on default command."); - } - - var nonUniqueOptionShortNames = commandSchema.Options - .Where(o => o.ShortName != null) - .Select(o => o.ShortName.Value) - .GroupBy(i => i) - .Where(g => g.Count() >= 2) - .SelectMany(g => g) - .Distinct() - .ToArray(); - - foreach (var optionShortName in nonUniqueOptionShortNames) - { - result.Add(!commandSchema.Name.IsNullOrWhiteSpace() - ? $"There are multiple options defined with short name [{optionShortName}] on command [{commandSchema.Name}]." - : $"There are multiple options defined with short name [{optionShortName}] on default command."); - } - } - - return result; - } - /// public async Task RunAsync(IReadOnlyList commandLineArguments) { @@ -129,23 +49,17 @@ namespace CliFx try { + // Get schemas for all available command types + var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes); + + // Parse command input from arguments var commandInput = _commandInputParser.ParseCommandInput(commandLineArguments); - var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes); - var matchingCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName); - - // Validate available command schemas - var validationErrors = GetAvailableCommandSchemasValidationErrors(availableCommandSchemas); - if (validationErrors.Any()) - { - foreach (var error in validationErrors) - _console.WithForegroundColor(ConsoleColor.Red, () => _console.Output.WriteLine(error)); - - return -1; - } + // Find command schema matching the name specified in the input + var targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName); // Handle cases where requested command is not defined - if (matchingCommandSchema == null) + if (targetCommandSchema == null) { var isError = false; @@ -164,7 +78,7 @@ namespace CliFx // Use a stub if parent command schema is not found if (parentCommandSchema == null) { - parentCommandSchema = _commandSchemaResolver.GetCommandSchema(typeof(StubDefaultCommand)); + parentCommandSchema = CommandSchema.StubDefaultCommand; availableCommandSchemas = availableCommandSchemas.Concat(new[] { parentCommandSchema }).ToArray(); } @@ -186,18 +100,19 @@ namespace CliFx // Show help if it was requested if (commandInput.IsHelpRequested()) { - var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, matchingCommandSchema); + var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema); _helpTextRenderer.RenderHelpText(_console, helpTextSource); return 0; } // Create an instance of the command - var command = _commandFactory.CreateCommand(matchingCommandSchema.Type); + var command = _commandFactory.CreateCommand(targetCommandSchema.Type); // Populate command with options according to its schema - _commandInitializer.InitializeCommand(command, matchingCommandSchema, commandInput); + _commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput); + // Execute command await command.ExecuteAsync(_console); return 0; @@ -221,13 +136,4 @@ namespace CliFx } } } - - public partial class CliApplication - { - [Command] - private sealed class StubDefaultCommand : ICommand - { - public Task ExecuteAsync(IConsole console) => Task.CompletedTask; - } - } } \ No newline at end of file diff --git a/CliFx/Exceptions/InvalidCommandSchemaException.cs b/CliFx/Exceptions/InvalidCommandSchemaException.cs new file mode 100644 index 0000000..aa98016 --- /dev/null +++ b/CliFx/Exceptions/InvalidCommandSchemaException.cs @@ -0,0 +1,26 @@ +using System; + +namespace CliFx.Exceptions +{ + /// + /// Thrown when a command schema fails validation. + /// + public class InvalidCommandSchemaException : CliFxException + { + /// + /// Initializes an instance of . + /// + public InvalidCommandSchemaException(string message) + : base(message) + { + } + + /// + /// Initializes an instance of . + /// + public InvalidCommandSchemaException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandOptionSchema.cs b/CliFx/Models/CommandOptionSchema.cs index ef4b0fb..7e30e9c 100644 --- a/CliFx/Models/CommandOptionSchema.cs +++ b/CliFx/Models/CommandOptionSchema.cs @@ -74,10 +74,10 @@ namespace CliFx.Models // We define them here to serve as a single source of truth, because they are used... // ...in CliApplication (when reading) and HelpTextRenderer (when writing). - internal static CommandOptionSchema Help { get; } = + internal static CommandOptionSchema HelpOption { get; } = new CommandOptionSchema(null, "help", 'h', false, "Shows help text."); - internal static CommandOptionSchema Version { get; } = + internal static CommandOptionSchema VersionOption { get; } = new CommandOptionSchema(null, "version", null, false, "Shows version information."); } } \ No newline at end of file diff --git a/CliFx/Models/CommandSchema.cs b/CliFx/Models/CommandSchema.cs index 79e043e..7cf4b51 100644 --- a/CliFx/Models/CommandSchema.cs +++ b/CliFx/Models/CommandSchema.cs @@ -8,7 +8,7 @@ namespace CliFx.Models /// /// Schema of a defined command. /// - public class CommandSchema + public partial class CommandSchema { /// /// Underlying type. @@ -60,4 +60,10 @@ namespace CliFx.Models return buffer.ToString(); } } + + public partial class CommandSchema + { + internal static CommandSchema StubDefaultCommand { get; } = + new CommandSchema(null, null, null, new CommandOptionSchema[0]); + } } \ No newline at end of file diff --git a/CliFx/Models/Extensions.cs b/CliFx/Models/Extensions.cs index 2e4f7e2..e8eefcd 100644 --- a/CliFx/Models/Extensions.cs +++ b/CliFx/Models/Extensions.cs @@ -111,7 +111,7 @@ namespace CliFx.Models var firstOption = commandInput.Options.FirstOrDefault(); - return firstOption != null && CommandOptionSchema.Help.MatchesAlias(firstOption.Alias); + return firstOption != null && CommandOptionSchema.HelpOption.MatchesAlias(firstOption.Alias); } /// @@ -123,7 +123,7 @@ namespace CliFx.Models var firstOption = commandInput.Options.FirstOrDefault(); - return firstOption != null && CommandOptionSchema.Version.MatchesAlias(firstOption.Alias); + return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias); } /// diff --git a/CliFx/Services/CommandSchemaResolver.cs b/CliFx/Services/CommandSchemaResolver.cs index e0c1def..feca8f1 100644 --- a/CliFx/Services/CommandSchemaResolver.cs +++ b/CliFx/Services/CommandSchemaResolver.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using CliFx.Attributes; +using CliFx.Exceptions; using CliFx.Internal; using CliFx.Models; @@ -26,11 +28,8 @@ namespace CliFx.Services attribute.Description); } - /// - public CommandSchema GetCommandSchema(Type commandType) + private CommandSchema GetCommandSchema(Type commandType) { - commandType.GuardNotNull(nameof(commandType)); - // Attribute is optional for commands in order to reduce runtime rule complexity var attribute = commandType.GetCustomAttribute(); @@ -41,5 +40,88 @@ namespace CliFx.Services attribute?.Description, options); } + + /// + public IReadOnlyList GetCommandSchemas(IReadOnlyList commandTypes) + { + commandTypes.GuardNotNull(nameof(commandTypes)); + + // Get command schemas + var commandSchemas = commandTypes.Select(GetCommandSchema).ToArray(); + + // Throw if there are no commands defined + if (!commandSchemas.Any()) + { + throw new InvalidCommandSchemaException("There are no commands defined."); + } + + // Throw if there are multiple commands with the same name + var nonUniqueCommandNames = commandSchemas + .Select(c => c.Name) + .GroupBy(i => i, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() >= 2) + .SelectMany(g => g) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + foreach (var commandName in nonUniqueCommandNames) + { + throw new InvalidCommandSchemaException(!commandName.IsNullOrWhiteSpace() + ? $"There are multiple commands defined with name [{commandName}]." + : "There are multiple default commands defined."); + } + + // Throw if there are commands that don't implement ICommand + var nonImplementedCommandNames = commandSchemas + .Where(c => !c.Type.Implements(typeof(ICommand))) + .Select(c => c.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + foreach (var commandName in nonImplementedCommandNames) + { + throw new InvalidCommandSchemaException(!commandName.IsNullOrWhiteSpace() + ? $"Command [{commandName}] doesn't implement ICommand." + : "Default command doesn't implement ICommand."); + } + + // Throw if there are multiple options with the same name inside the same command + foreach (var commandSchema in commandSchemas) + { + var nonUniqueOptionNames = commandSchema.Options + .Where(o => !o.Name.IsNullOrWhiteSpace()) + .Select(o => o.Name) + .GroupBy(i => i, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() >= 2) + .SelectMany(g => g) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + foreach (var optionName in nonUniqueOptionNames) + { + throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace() + ? $"There are multiple options defined with name [{optionName}] on command [{commandSchema.Name}]." + : $"There are multiple options defined with name [{optionName}] on default command."); + } + + var nonUniqueOptionShortNames = commandSchema.Options + .Where(o => o.ShortName != null) + .Select(o => o.ShortName.Value) + .GroupBy(i => i) + .Where(g => g.Count() >= 2) + .SelectMany(g => g) + .Distinct() + .ToArray(); + + foreach (var optionShortName in nonUniqueOptionShortNames) + { + throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace() + ? $"There are multiple options defined with short name [{optionShortName}] on command [{commandSchema.Name}]." + : $"There are multiple options defined with short name [{optionShortName}] on default command."); + } + } + + return commandSchemas; + } } } \ No newline at end of file diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs index 6a35c5e..9e37175 100644 --- a/CliFx/Services/Extensions.cs +++ b/CliFx/Services/Extensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using CliFx.Internal; using CliFx.Models; @@ -12,15 +11,14 @@ namespace CliFx.Services public static class Extensions { /// - /// Resolves command schemas for commands of specified types. + /// Resolves command schema for specified command type. /// - public static IReadOnlyList GetCommandSchemas(this ICommandSchemaResolver resolver, - IReadOnlyList commandTypes) + public static CommandSchema GetCommandSchema(this ICommandSchemaResolver resolver, Type commandType) { resolver.GuardNotNull(nameof(resolver)); - commandTypes.GuardNotNull(nameof(commandTypes)); + commandType.GuardNotNull(nameof(commandType)); - return commandTypes.Select(resolver.GetCommandSchema).ToArray(); + return resolver.GetCommandSchemas(new[] {commandType}).First(); } /// diff --git a/CliFx/Services/HelpTextRenderer.cs b/CliFx/Services/HelpTextRenderer.cs index 89603e5..da6d6c9 100644 --- a/CliFx/Services/HelpTextRenderer.cs +++ b/CliFx/Services/HelpTextRenderer.cs @@ -22,9 +22,9 @@ namespace CliFx.Services var row = 0; // Get built-in option schemas (help and version) - var builtInOptionSchemas = new List { CommandOptionSchema.Help }; + var builtInOptionSchemas = new List {CommandOptionSchema.HelpOption}; if (source.TargetCommandSchema.IsDefault()) - builtInOptionSchemas.Add(CommandOptionSchema.Version); + builtInOptionSchemas.Add(CommandOptionSchema.VersionOption); // Get child command schemas var childCommandSchemas = source.AvailableCommandSchemas diff --git a/CliFx/Services/ICommandSchemaResolver.cs b/CliFx/Services/ICommandSchemaResolver.cs index 3ffbccc..066c417 100644 --- a/CliFx/Services/ICommandSchemaResolver.cs +++ b/CliFx/Services/ICommandSchemaResolver.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using CliFx.Models; namespace CliFx.Services @@ -9,8 +10,8 @@ namespace CliFx.Services public interface ICommandSchemaResolver { /// - /// Resolves schema of a command of specified type. + /// Resolves schemas of specified command types. /// - CommandSchema GetCommandSchema(Type commandType); + IReadOnlyList GetCommandSchemas(IReadOnlyList commandTypes); } } \ No newline at end of file