mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Rework CommandSchemaResolver and move validation there
This commit is contained in:
@@ -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
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="ICliApplication"/>.
|
||||
/// </summary>
|
||||
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<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 />
|
||||
public async Task<int> RunAsync(IReadOnlyList<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
CliFx/Exceptions/InvalidCommandSchemaException.cs
Normal file
26
CliFx/Exceptions/InvalidCommandSchemaException.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace CliFx.Models
|
||||
/// <summary>
|
||||
/// Schema of a defined command.
|
||||
/// </summary>
|
||||
public class CommandSchema
|
||||
public partial class CommandSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// 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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<CommandAttribute>();
|
||||
|
||||
@@ -41,5 +40,88 @@ namespace CliFx.Services
|
||||
attribute?.Description,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves command schemas for commands of specified types.
|
||||
/// Resolves command schema for specified command type.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<CommandSchema> GetCommandSchemas(this ICommandSchemaResolver resolver,
|
||||
IReadOnlyList<Type> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -22,9 +22,9 @@ namespace CliFx.Services
|
||||
var row = 0;
|
||||
|
||||
// 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())
|
||||
builtInOptionSchemas.Add(CommandOptionSchema.Version);
|
||||
builtInOptionSchemas.Add(CommandOptionSchema.VersionOption);
|
||||
|
||||
// Get child command schemas
|
||||
var childCommandSchemas = source.AvailableCommandSchemas
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves schema of a command of specified type.
|
||||
/// Resolves schemas of specified command types.
|
||||
/// </summary>
|
||||
CommandSchema GetCommandSchema(Type commandType);
|
||||
IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user