mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Rework schema validation in CommandSchemaResolver
This commit is contained in:
@@ -13,6 +13,9 @@ namespace CliFx.Tests.Services
|
||||
[CommandOption("option-a", 'a')]
|
||||
public int OptionA { get; set; }
|
||||
|
||||
[CommandOption('A')]
|
||||
public int AlmostOptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", IsRequired = true)]
|
||||
public string OptionB { get; set; }
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace CliFx.Tests.Services
|
||||
{
|
||||
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionA)),
|
||||
"option-a", 'a', false, null),
|
||||
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.AlmostOptionA)),
|
||||
null, 'A', false, null),
|
||||
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionB)),
|
||||
"option-b", null, true, null)
|
||||
}),
|
||||
@@ -93,7 +95,7 @@ namespace CliFx.Tests.Services
|
||||
|
||||
// Act & Assert
|
||||
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
|
||||
.Should().ThrowExactly<InvalidCommandSchemaException>();
|
||||
.Should().ThrowExactly<SchemaValidationException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
CliFx/Exceptions/SchemaValidationException.cs
Normal file
26
CliFx/Exceptions/SchemaValidationException.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when a command schema fails validation.
|
||||
/// </summary>
|
||||
public class SchemaValidationException : CliFxException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="SchemaValidationException"/>.
|
||||
/// </summary>
|
||||
public SchemaValidationException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="SchemaValidationException"/>.
|
||||
/// </summary>
|
||||
public SchemaValidationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,6 @@ namespace CliFx.Internal
|
||||
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
|
||||
builder.Length > 0 ? builder.Append(value) : builder;
|
||||
|
||||
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
|
||||
dic.TryGetValue(key, out var result) ? result : default;
|
||||
|
||||
public static IEnumerable<T> ExceptNull<T>(this IEnumerable<T> source) where T : class => source.Where(i => i != null);
|
||||
|
||||
public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, T value)
|
||||
{
|
||||
foreach (var i in source)
|
||||
@@ -51,7 +46,7 @@ namespace CliFx.Internal
|
||||
|
||||
return type.GetInterfaces()
|
||||
.Select(GetEnumerableUnderlyingType)
|
||||
.ExceptNull()
|
||||
.Where(t => t != default)
|
||||
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
@@ -14,84 +14,54 @@ namespace CliFx.Services
|
||||
/// </summary>
|
||||
public class CommandSchemaResolver : ICommandSchemaResolver
|
||||
{
|
||||
private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty)
|
||||
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
|
||||
{
|
||||
var attribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
|
||||
var result = new List<CommandOptionSchema>();
|
||||
|
||||
// If an attribute is not set, then it's not an option so we just skip it
|
||||
if (attribute == null)
|
||||
return null;
|
||||
|
||||
return new CommandOptionSchema(optionProperty,
|
||||
attribute.Name,
|
||||
attribute.ShortName,
|
||||
attribute.IsRequired,
|
||||
attribute.Description);
|
||||
}
|
||||
|
||||
private CommandSchema GetCommandSchema(Type commandType)
|
||||
{
|
||||
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||
|
||||
// Make sure attribute is set
|
||||
if (attribute == null)
|
||||
foreach (var property in commandType.GetProperties())
|
||||
{
|
||||
throw new InvalidCommandSchemaException($"Command type [{commandType}] must be annotated with [{typeof(CommandAttribute)}].");
|
||||
var attribute = property.GetCustomAttribute<CommandOptionAttribute>();
|
||||
|
||||
// If an attribute is not set, then it's not an option so we just skip it
|
||||
if (attribute == null)
|
||||
continue;
|
||||
|
||||
// Build option schema
|
||||
var optionSchema = new CommandOptionSchema(property,
|
||||
attribute.Name,
|
||||
attribute.ShortName,
|
||||
attribute.IsRequired,
|
||||
attribute.Description);
|
||||
|
||||
// Make sure there are no other options with the same name
|
||||
var existingOptionWithSameName = result
|
||||
.Where(o => !o.Name.IsNullOrWhiteSpace())
|
||||
.FirstOrDefault(o => string.Equals(o.Name, optionSchema.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingOptionWithSameName != null)
|
||||
{
|
||||
throw new SchemaValidationException(
|
||||
$"Command type [{commandType}] has options defined with the same name: " +
|
||||
$"[{existingOptionWithSameName.Property}] and [{optionSchema.Property}].");
|
||||
}
|
||||
|
||||
// Make sure there are no other options with the same short name
|
||||
var existingOptionWithSameShortName = result
|
||||
.Where(o => o.ShortName != null)
|
||||
.FirstOrDefault(o => o.ShortName == optionSchema.ShortName);
|
||||
|
||||
if (existingOptionWithSameShortName != null)
|
||||
{
|
||||
throw new SchemaValidationException(
|
||||
$"Command type [{commandType}] has options defined with the same short name: " +
|
||||
$"[{existingOptionWithSameShortName.Property}] and [{optionSchema.Property}].");
|
||||
}
|
||||
|
||||
// Add schema to list
|
||||
result.Add(optionSchema);
|
||||
}
|
||||
|
||||
// Get option schemas
|
||||
var options = commandType.GetProperties().Select(GetCommandOptionSchema).ExceptNull().ToArray();
|
||||
|
||||
// Create command schema
|
||||
var commandSchema = new CommandSchema(commandType,
|
||||
attribute.Name,
|
||||
attribute.Description,
|
||||
options);
|
||||
|
||||
// Make sure command type implements ICommand.
|
||||
// (we check using command schema to provide a more useful error message)
|
||||
if (!commandSchema.Type.Implements(typeof(ICommand)))
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace()
|
||||
? $"Command [{commandSchema.Name}] doesn't implement ICommand."
|
||||
: "Default command doesn't implement ICommand.");
|
||||
}
|
||||
|
||||
// Make sure there are no options with duplicate names
|
||||
var nonUniqueOptionName = 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)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (nonUniqueOptionName != null)
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace()
|
||||
? $"There are multiple options defined with name [{nonUniqueOptionName}] on command [{commandSchema.Name}]."
|
||||
: $"There are multiple options defined with name [{nonUniqueOptionName}] on default command.");
|
||||
}
|
||||
|
||||
// Make sure there are no options with duplicate short names
|
||||
var nonUniqueOptionShortName = commandSchema.Options
|
||||
.Where(o => o.ShortName != null)
|
||||
.Select(o => o.ShortName)
|
||||
.GroupBy(i => i)
|
||||
.Where(g => g.Count() >= 2)
|
||||
.SelectMany(g => g)
|
||||
.Distinct()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (nonUniqueOptionShortName != null)
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!commandSchema.Name.IsNullOrWhiteSpace()
|
||||
? $"There are multiple options defined with short name [{nonUniqueOptionShortName}] on command [{commandSchema.Name}]."
|
||||
: $"There are multiple options defined with short name [{nonUniqueOptionShortName}] on default command.");
|
||||
}
|
||||
|
||||
return commandSchema;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -99,32 +69,57 @@ namespace CliFx.Services
|
||||
{
|
||||
commandTypes.GuardNotNull(nameof(commandTypes));
|
||||
|
||||
// Throw if there are no command types specified
|
||||
// Make sure there's at least one command defined
|
||||
if (!commandTypes.Any())
|
||||
{
|
||||
throw new InvalidCommandSchemaException("There are no commands defined.");
|
||||
throw new SchemaValidationException("There are no commands defined.");
|
||||
}
|
||||
|
||||
// Get command schemas
|
||||
var commandSchemas = commandTypes.Select(GetCommandSchema).ToArray();
|
||||
var result = new List<CommandSchema>();
|
||||
|
||||
// Make sure there are no commands with duplicate names
|
||||
var nonUniqueCommandName = commandSchemas
|
||||
.Select(c => c.Name)
|
||||
.GroupBy(i => i, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(g => g.Count() >= 2)
|
||||
.SelectMany(g => g)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (nonUniqueCommandName != null)
|
||||
foreach (var commandType in commandTypes)
|
||||
{
|
||||
throw new InvalidCommandSchemaException(!nonUniqueCommandName.IsNullOrWhiteSpace()
|
||||
? $"There are multiple commands defined with name [{nonUniqueCommandName}]."
|
||||
: "There are multiple default commands defined.");
|
||||
// Make sure command type implements ICommand.
|
||||
if (!commandType.Implements(typeof(ICommand)))
|
||||
{
|
||||
throw new SchemaValidationException(
|
||||
$"Command type [{commandType}] must implement {typeof(ICommand)}.");
|
||||
}
|
||||
|
||||
// Get attribute
|
||||
var attribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||
|
||||
// Make sure attribute is set
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new SchemaValidationException(
|
||||
$"Command type [{commandType}] must be annotated with [{typeof(CommandAttribute)}].");
|
||||
}
|
||||
|
||||
// Get option schemas
|
||||
var optionSchemas = GetCommandOptionSchemas(commandType);
|
||||
|
||||
// Build command schema
|
||||
var commandSchema = new CommandSchema(commandType,
|
||||
attribute.Name,
|
||||
attribute.Description,
|
||||
optionSchemas);
|
||||
|
||||
// Make sure there are no other commands with the same name
|
||||
var existingCommandWithSameName = result
|
||||
.FirstOrDefault(c => string.Equals(c.Name, commandSchema.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingCommandWithSameName != null)
|
||||
{
|
||||
throw new SchemaValidationException(
|
||||
$"Command type [{existingCommandWithSameName.Type}] has the same name as another command type [{commandType}].");
|
||||
}
|
||||
|
||||
// Add schema to list
|
||||
result.Add(commandSchema);
|
||||
}
|
||||
|
||||
return commandSchemas;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user