Files
spectre.console/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs
2023-11-28 00:14:54 +01:00

247 lines
8.8 KiB
C#

namespace Spectre.Console.Cli;
internal static class CommandModelBuilder
{
// Consider removing this in favor for value tuples at some point.
private sealed class OrderedProperties
{
public int Level { get; }
public int SortOrder { get; }
public PropertyInfo[] Properties { get; }
public OrderedProperties(int level, int sortOrder, PropertyInfo[] properties)
{
Level = level;
SortOrder = sortOrder;
Properties = properties;
}
}
public static CommandModel Build(IConfiguration configuration)
{
var result = new List<CommandInfo>();
foreach (var command in configuration.Commands)
{
result.Add(Build(null, command));
}
if (configuration.DefaultCommand != null)
{
// Add the examples from the configuration to the default command.
configuration.DefaultCommand.Examples.AddRange(configuration.Examples);
// Build the default command.
var defaultCommand = Build(null, configuration.DefaultCommand);
result.Add(defaultCommand);
}
// Create the command model and validate it.
var model = new CommandModel(configuration.Settings, result, configuration.Examples);
CommandModelValidator.Validate(model, configuration.Settings);
return model;
}
private static CommandInfo Build(CommandInfo? parent, ConfiguredCommand command)
{
var info = new CommandInfo(parent, command);
foreach (var parameter in GetParameters(info))
{
info.Parameters.Add(parameter);
}
foreach (var childCommand in command.Children)
{
var child = Build(info, childCommand);
info.Children.Add(child);
}
// Normalize argument positions.
var index = 0;
foreach (var argument in info.Parameters.OfType<CommandArgument>()
.OrderBy(argument => argument.Position))
{
argument.Position = index;
index++;
}
return info;
}
private static IEnumerable<CommandParameter> GetParameters(CommandInfo command)
{
var result = new List<CommandParameter>();
var argumentPosition = 0;
// We need to get parameters in order of the class where they were defined.
// We assign each inheritance level a value that is used to properly sort the
// arguments when iterating over them.
IEnumerable<OrderedProperties> GetPropertiesInOrder()
{
var current = command.SettingsType;
var level = 0;
var sortOrder = 0;
while (current.BaseType != null)
{
yield return new OrderedProperties(level, sortOrder, current.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public));
current = current.BaseType;
// Things get a little bit complicated now.
// Only consider a setting's base type part of the
// setting, if there isn't a parent command that implements
// the setting's base type. This might come back to bite us :)
var currentCommand = command.Parent;
while (currentCommand != null)
{
if (currentCommand.SettingsType == current)
{
level--;
break;
}
currentCommand = currentCommand.Parent;
}
sortOrder--;
}
}
var groups = GetPropertiesInOrder();
foreach (var group in groups.OrderBy(x => x.Level).ThenBy(x => x.SortOrder))
{
var parameters = new List<CommandParameter>();
foreach (var property in group.Properties)
{
if (property.IsDefined(typeof(CommandOptionAttribute)))
{
var attribute = property.GetCustomAttribute<CommandOptionAttribute>();
if (attribute != null)
{
var option = BuildOptionParameter(property, attribute);
// Any previous command has this option defined?
if (command.HaveParentWithOption(option))
{
// Do we allow it to exist on this command as well?
if (command.AllowParentOption(option))
{
option.IsShadowed = true;
parameters.Add(option);
}
}
else
{
// No parent have this option.
parameters.Add(option);
}
}
}
else if (property.IsDefined(typeof(CommandArgumentAttribute)))
{
var attribute = property.GetCustomAttribute<CommandArgumentAttribute>();
if (attribute != null)
{
var argument = BuildArgumentParameter(property, attribute);
// Any previous command has this argument defined?
// In that case, we should not assign the parameter to this command.
if (!command.HaveParentWithArgument(argument))
{
parameters.Add(argument);
}
}
}
}
// Update the position for the parameters.
foreach (var argument in parameters.OfType<CommandArgument>().OrderBy(x => x.Position))
{
argument.Position = argumentPosition++;
}
// Add all parameters to the result.
foreach (var groupResult in parameters)
{
result.Add(groupResult);
}
}
return result;
}
private static CommandOption BuildOptionParameter(PropertyInfo property, CommandOptionAttribute attribute)
{
var description = property.GetCustomAttribute<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var deconstructor = property.GetCustomAttribute<PairDeconstructorAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true);
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var kind = GetOptionKind(property.PropertyType, attribute, deconstructor, converter);
if (defaultValue == null && property.PropertyType == typeof(bool))
{
defaultValue = new DefaultValueAttribute(false);
}
return new CommandOption(property.PropertyType, kind,
property, description?.Description, converter, deconstructor,
attribute, valueProvider, validators, defaultValue,
attribute.ValueIsOptional);
}
private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute)
{
var description = property.GetCustomAttribute<DescriptionAttribute>();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
var validators = property.GetCustomAttributes<ParameterValidationAttribute>(true);
var kind = GetParameterKind(property.PropertyType);
return new CommandArgument(
property.PropertyType, kind, property,
description?.Description, converter,
defaultValue, attribute, valueProvider,
validators);
}
private static ParameterKind GetOptionKind(
Type type,
CommandOptionAttribute attribute,
PairDeconstructorAttribute? deconstructor,
TypeConverterAttribute? converter)
{
if (attribute.ValueIsOptional)
{
return ParameterKind.FlagWithValue;
}
if (type.IsPairDeconstructable() && (deconstructor != null || converter == null))
{
return ParameterKind.Pair;
}
return GetParameterKind(type);
}
private static ParameterKind GetParameterKind(Type type)
{
if (type == typeof(bool) || type == typeof(bool?))
{
return ParameterKind.Flag;
}
if (type.IsArray)
{
return ParameterKind.Vector;
}
return ParameterKind.Scalar;
}
}