Files
CliFx/CliFx/Schema/CommandSchema.cs
Tyrrrz a62ce71424 asd
2024-09-03 02:03:50 +03:00

232 lines
7.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Parsing;
using CliFx.Utils.Extensions;
namespace CliFx.Schema;
/// <summary>
/// Describes an individual command, along with its inputs.
/// </summary>
public class CommandSchema(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type,
string? name,
string? description,
IReadOnlyList<CommandInputSchema> inputs
)
{
/// <summary>
/// Underlying CLR type of the command.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type Type { get; } = type;
/// <summary>
/// Command name.
/// </summary>
public string? Name { get; } = name;
/// <summary>
/// Whether the command is the application's default command.
/// </summary>
public bool IsDefault { get; } = string.IsNullOrWhiteSpace(name);
/// <summary>
/// Command description.
/// </summary>
public string? Description { get; } = description;
/// <summary>
/// Command inputs.
/// </summary>
public IReadOnlyList<CommandInputSchema> Inputs { get; } = inputs;
/// <summary>
/// Parameter inputs of the command.
/// </summary>
public IReadOnlyList<CommandParameterSchema> Parameters { get; } =
inputs.OfType<CommandParameterSchema>().ToArray();
/// <summary>
/// Option inputs of the command.
/// </summary>
public IReadOnlyList<CommandOptionSchema> Options { get; } =
inputs.OfType<CommandOptionSchema>().ToArray();
internal bool MatchesName(string? name) =>
!string.IsNullOrWhiteSpace(Name)
? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase)
: string.IsNullOrWhiteSpace(name);
internal IReadOnlyDictionary<CommandInputSchema, object?> GetValues(ICommand instance) =>
Inputs.ToDictionary(input => input, input => input.Property.GetValue(instance));
private void ActivateParameters(ICommand instance, CommandArguments arguments)
{
// Ensure there are no unexpected parameters and that all parameters are provided
var remainingParameterTokens = arguments.Parameters.ToList();
var remainingRequiredParameters = Parameters.Where(p => p.IsRequired).ToList();
var position = 0;
foreach (var parameter in Parameters.OrderBy(p => p.Order))
{
// Break when there are no remaining tokens
if (position >= arguments.Parameters.Count)
break;
// Sequential: take all remaining tokens starting from the current position
if (parameter.IsSequence)
{
var parameterTokens = arguments.Parameters.Skip(position).ToArray();
parameter.Activate(instance, parameterTokens.Select(p => p.Value).ToArray());
position += parameterTokens.Length;
remainingParameterTokens.RemoveRange(parameterTokens);
}
// Non-sequential: take one token at the current position
else
{
var parameterToken = arguments.Parameters[position];
parameter.Activate(instance, [parameterToken.Value]);
position++;
remainingParameterTokens.Remove(parameterToken);
}
remainingRequiredParameters.Remove(parameter);
}
if (remainingParameterTokens.Any())
{
throw CliFxException.UserError(
$"""
Unexpected parameter(s):
{remainingParameterTokens.Select(p => p.FormattedIdentifier).JoinToString(" ")}
"""
);
}
if (remainingRequiredParameters.Any())
{
throw CliFxException.UserError(
$"""
Missing required parameter(s):
{remainingRequiredParameters
.Select(p => p.GetFormattedIdentifier())
.JoinToString(" ")}
"""
);
}
}
private void ActivateOptions(
ICommand instance,
CommandArguments arguments,
IReadOnlyDictionary<string, string?> environmentVariables
)
{
// Ensure there are no unrecognized options and that all required options are set
var remainingOptionTokens = arguments.Options.ToList();
var remainingRequiredOptions = Options.Where(o => o.IsRequired).ToList();
foreach (var option in Options)
{
var optionTokens = arguments
.Options.Where(o => option.MatchesIdentifier(o.Identifier))
.ToArray();
var environmentVariable = environmentVariables.FirstOrDefault(v =>
option.MatchesEnvironmentVariable(v.Key)
);
// From arguments
if (optionTokens.Any())
{
var rawValues = optionTokens.SelectMany(o => o.Values).ToArray();
option.Activate(instance, rawValues);
// Required options need at least one value to be set
if (rawValues.Any())
remainingRequiredOptions.Remove(option);
}
// From environment
else if (!string.IsNullOrEmpty(environmentVariable.Value))
{
var rawValues = !option.IsSequence
? [environmentVariable.Value]
: environmentVariable.Value.Split(
Path.PathSeparator,
StringSplitOptions.RemoveEmptyEntries
);
option.Activate(instance, rawValues);
// Required options need at least one value to be set
if (rawValues.Any())
remainingRequiredOptions.Remove(option);
}
// No input, skip
else
{
continue;
}
remainingOptionTokens.RemoveRange(optionTokens);
}
if (remainingOptionTokens.Any())
{
throw CliFxException.UserError(
$"""
Unrecognized option(s):
{remainingOptionTokens.Select(o => o.FormattedIdentifier).JoinToString(", ")}
"""
);
}
if (remainingRequiredOptions.Any())
{
throw CliFxException.UserError(
$"""
Missing required option(s):
{remainingRequiredOptions
.Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")}
"""
);
}
}
internal void Activate(
ICommand instance,
CommandArguments arguments,
IReadOnlyDictionary<string, string?> environmentVariables
)
{
ActivateParameters(instance, arguments);
ActivateOptions(instance, arguments, environmentVariables);
}
/// <inheritdoc />
[ExcludeFromCodeCoverage]
public override string ToString() => Name ?? "{default}";
}
/// <inheritdoc cref="CommandSchema" />
/// <remarks>
/// Generic version of the type is used to simplify initialization from source-generated code and
/// to enforce static references to all types used in the binding.
/// The non-generic version is used internally by the framework when operating in a dynamic context.
/// </remarks>
public class CommandSchema<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TCommand
>(string? name, string? description, IReadOnlyList<CommandInputSchema> inputs)
: CommandSchema(typeof(TCommand), name, description, inputs)
where TCommand : ICommand;