Rework CommandSchemaResolver and move validation there

This commit is contained in:
Alexey Golub
2019-08-19 01:15:10 +03:00
parent de8513c6fa
commit 66f9b1a256
9 changed files with 145 additions and 126 deletions

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Internal; using CliFx.Internal;
using CliFx.Models; using CliFx.Models;
@@ -13,7 +12,7 @@ namespace CliFx
/// <summary> /// <summary>
/// Default implementation of <see cref="ICliApplication"/>. /// Default implementation of <see cref="ICliApplication"/>.
/// </summary> /// </summary>
public partial class CliApplication : ICliApplication public class CliApplication : ICliApplication
{ {
private readonly ApplicationMetadata _metadata; private readonly ApplicationMetadata _metadata;
private readonly ApplicationConfiguration _configuration; private readonly ApplicationConfiguration _configuration;
@@ -43,85 +42,6 @@ namespace CliFx
_helpTextRenderer = helpTextRenderer.GuardNotNull(nameof(helpTextRenderer)); _helpTextRenderer = helpTextRenderer.GuardNotNull(nameof(helpTextRenderer));
} }
private IReadOnlyList<string> GetAvailableCommandSchemasValidationErrors(IReadOnlyList<CommandSchema> availableCommandSchemas)
{
var result = new List<string>();
// 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;
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments) public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
{ {
@@ -129,23 +49,17 @@ namespace CliFx
try 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 commandInput = _commandInputParser.ParseCommandInput(commandLineArguments);
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes); // Find command schema matching the name specified in the input
var matchingCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName); var targetCommandSchema = 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;
}
// Handle cases where requested command is not defined // Handle cases where requested command is not defined
if (matchingCommandSchema == null) if (targetCommandSchema == null)
{ {
var isError = false; var isError = false;
@@ -164,7 +78,7 @@ namespace CliFx
// Use a stub if parent command schema is not found // Use a stub if parent command schema is not found
if (parentCommandSchema == null) if (parentCommandSchema == null)
{ {
parentCommandSchema = _commandSchemaResolver.GetCommandSchema(typeof(StubDefaultCommand)); parentCommandSchema = CommandSchema.StubDefaultCommand;
availableCommandSchemas = availableCommandSchemas.Concat(new[] { parentCommandSchema }).ToArray(); availableCommandSchemas = availableCommandSchemas.Concat(new[] { parentCommandSchema }).ToArray();
} }
@@ -186,18 +100,19 @@ namespace CliFx
// Show help if it was requested // Show help if it was requested
if (commandInput.IsHelpRequested()) if (commandInput.IsHelpRequested())
{ {
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, matchingCommandSchema); var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema);
_helpTextRenderer.RenderHelpText(_console, helpTextSource); _helpTextRenderer.RenderHelpText(_console, helpTextSource);
return 0; return 0;
} }
// Create an instance of the command // 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 // Populate command with options according to its schema
_commandInitializer.InitializeCommand(command, matchingCommandSchema, commandInput); _commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
// Execute command
await command.ExecuteAsync(_console); await command.ExecuteAsync(_console);
return 0; 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;
}
}
} }

View File

@@ -0,0 +1,26 @@
using System;
namespace CliFx.Exceptions
{
/// <summary>
/// Thrown when a command schema fails validation.
/// </summary>
public class InvalidCommandSchemaException : CliFxException
{
/// <summary>
/// Initializes an instance of <see cref="InvalidCommandSchemaException"/>.
/// </summary>
public InvalidCommandSchemaException(string message)
: base(message)
{
}
/// <summary>
/// Initializes an instance of <see cref="InvalidCommandSchemaException"/>.
/// </summary>
public InvalidCommandSchemaException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -74,10 +74,10 @@ namespace CliFx.Models
// We define them here to serve as a single source of truth, because they are used... // We define them here to serve as a single source of truth, because they are used...
// ...in CliApplication (when reading) and HelpTextRenderer (when writing). // ...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."); 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."); new CommandOptionSchema(null, "version", null, false, "Shows version information.");
} }
} }

View File

@@ -8,7 +8,7 @@ namespace CliFx.Models
/// <summary> /// <summary>
/// Schema of a defined command. /// Schema of a defined command.
/// </summary> /// </summary>
public class CommandSchema public partial class CommandSchema
{ {
/// <summary> /// <summary>
/// Underlying type. /// Underlying type.
@@ -60,4 +60,10 @@ namespace CliFx.Models
return buffer.ToString(); return buffer.ToString();
} }
} }
public partial class CommandSchema
{
internal static CommandSchema StubDefaultCommand { get; } =
new CommandSchema(null, null, null, new CommandOptionSchema[0]);
}
} }

View File

@@ -111,7 +111,7 @@ namespace CliFx.Models
var firstOption = commandInput.Options.FirstOrDefault(); var firstOption = commandInput.Options.FirstOrDefault();
return firstOption != null && CommandOptionSchema.Help.MatchesAlias(firstOption.Alias); return firstOption != null && CommandOptionSchema.HelpOption.MatchesAlias(firstOption.Alias);
} }
/// <summary> /// <summary>
@@ -123,7 +123,7 @@ namespace CliFx.Models
var firstOption = commandInput.Options.FirstOrDefault(); var firstOption = commandInput.Options.FirstOrDefault();
return firstOption != null && CommandOptionSchema.Version.MatchesAlias(firstOption.Alias); return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias);
} }
/// <summary> /// <summary>

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Internal; using CliFx.Internal;
using CliFx.Models; using CliFx.Models;
@@ -26,11 +28,8 @@ namespace CliFx.Services
attribute.Description); attribute.Description);
} }
/// <inheritdoc /> private CommandSchema GetCommandSchema(Type commandType)
public CommandSchema GetCommandSchema(Type commandType)
{ {
commandType.GuardNotNull(nameof(commandType));
// Attribute is optional for commands in order to reduce runtime rule complexity // Attribute is optional for commands in order to reduce runtime rule complexity
var attribute = commandType.GetCustomAttribute<CommandAttribute>(); var attribute = commandType.GetCustomAttribute<CommandAttribute>();
@@ -41,5 +40,88 @@ namespace CliFx.Services
attribute?.Description, attribute?.Description,
options); options);
} }
/// <inheritdoc />
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> 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;
}
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using CliFx.Internal; using CliFx.Internal;
using CliFx.Models; using CliFx.Models;
@@ -12,15 +11,14 @@ namespace CliFx.Services
public static class Extensions public static class Extensions
{ {
/// <summary> /// <summary>
/// Resolves command schemas for commands of specified types. /// Resolves command schema for specified command type.
/// </summary> /// </summary>
public static IReadOnlyList<CommandSchema> GetCommandSchemas(this ICommandSchemaResolver resolver, public static CommandSchema GetCommandSchema(this ICommandSchemaResolver resolver, Type commandType)
IReadOnlyList<Type> commandTypes)
{ {
resolver.GuardNotNull(nameof(resolver)); resolver.GuardNotNull(nameof(resolver));
commandTypes.GuardNotNull(nameof(commandTypes)); commandType.GuardNotNull(nameof(commandType));
return commandTypes.Select(resolver.GetCommandSchema).ToArray(); return resolver.GetCommandSchemas(new[] {commandType}).First();
} }
/// <summary> /// <summary>

View File

@@ -22,9 +22,9 @@ namespace CliFx.Services
var row = 0; var row = 0;
// Get built-in option schemas (help and version) // Get built-in option schemas (help and version)
var builtInOptionSchemas = new List<CommandOptionSchema> { CommandOptionSchema.Help }; var builtInOptionSchemas = new List<CommandOptionSchema> {CommandOptionSchema.HelpOption};
if (source.TargetCommandSchema.IsDefault()) if (source.TargetCommandSchema.IsDefault())
builtInOptionSchemas.Add(CommandOptionSchema.Version); builtInOptionSchemas.Add(CommandOptionSchema.VersionOption);
// Get child command schemas // Get child command schemas
var childCommandSchemas = source.AvailableCommandSchemas var childCommandSchemas = source.AvailableCommandSchemas

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using CliFx.Models; using CliFx.Models;
namespace CliFx.Services namespace CliFx.Services
@@ -9,8 +10,8 @@ namespace CliFx.Services
public interface ICommandSchemaResolver public interface ICommandSchemaResolver
{ {
/// <summary> /// <summary>
/// Resolves schema of a command of specified type. /// Resolves schemas of specified command types.
/// </summary> /// </summary>
CommandSchema GetCommandSchema(Type commandType); IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes);
} }
} }