From 034d3cec6646c2afc197c9b0d0ea128439550b74 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Sun, 16 Jun 2024 02:16:43 +0300 Subject: [PATCH] asd --- CliFx.Demo/CliFx.Demo.csproj | 1 + CliFx/CliApplication.cs | 4 +- CliFx/CommandBinder.cs | 389 ----------------------- CliFx/Formatting/HelpConsoleFormatter.cs | 69 ++-- CliFx/ICommand.cs | 11 + CliFx/ICommandWithVersionOption.cs | 2 +- CliFx/Infrastructure/SystemConsole.cs | 4 + CliFx/Input/CommandInput.cs | 5 +- CliFx/Input/DirectiveInput.cs | 2 +- CliFx/Input/EnvironmentVariableInput.cs | 2 +- CliFx/Input/OptionInput.cs | 13 +- CliFx/Input/ParameterInput.cs | 10 +- CliFx/Schema/CommandSchema.cs | 2 +- CliFx/Schema/InputSchema.cs | 2 +- CliFx/Schema/OptionSchema.cs | 2 +- CliFx/Schema/ParameterSchema.cs | 2 +- CliFx/Schema/PropertyBinding.cs | 5 +- 17 files changed, 85 insertions(+), 440 deletions(-) delete mode 100644 CliFx/CommandBinder.cs diff --git a/CliFx.Demo/CliFx.Demo.csproj b/CliFx.Demo/CliFx.Demo.csproj index 4d4439a..32de33e 100644 --- a/CliFx.Demo/CliFx.Demo.csproj +++ b/CliFx.Demo/CliFx.Demo.csproj @@ -4,6 +4,7 @@ Exe net8.0 ../favicon.ico + true diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 57a4063..9e9859b 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -23,8 +23,6 @@ public class CliApplication( ITypeActivator typeActivator ) { - private readonly CommandBinder _commandBinder = new(typeActivator); - /// /// Application metadata. /// @@ -116,7 +114,7 @@ public class CliApplication( try { // Bind the command input to the command instance - _commandBinder.Bind(commandInput, commandSchema, commandInstance); + commandInstance.Bind(commandSchema, commandInput); // Handle the version option if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true }) diff --git a/CliFx/CommandBinder.cs b/CliFx/CommandBinder.cs deleted file mode 100644 index 202f123..0000000 --- a/CliFx/CommandBinder.cs +++ /dev/null @@ -1,389 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using CliFx.Exceptions; -using CliFx.Extensibility; -using CliFx.Infrastructure; -using CliFx.Input; -using CliFx.Schema; -using CliFx.Utils.Extensions; - -namespace CliFx; - -internal class CommandBinder(ITypeActivator typeActivator) -{ - private readonly IFormatProvider _formatProvider = CultureInfo.InvariantCulture; - - private object? ConvertSingle(InputSchema inputSchema, string? rawValue, Type targetType) - { - // Custom converter - if (inputSchema.Converter is not null) - { - return inputSchema.Converter.Convert(rawValue); - } - - // Assignable from a string (e.g. string itself, object, etc) - if (targetType.IsAssignableFrom(typeof(string))) - { - return rawValue; - } - - // Special case for bool - if (targetType == typeof(bool)) - { - return string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue); - } - - // Special case for DateTimeOffset - if (targetType == typeof(DateTimeOffset)) - { - // Null reference exception will be handled upstream - return DateTimeOffset.Parse(rawValue!, _formatProvider); - } - - // Special case for TimeSpan - if (targetType == typeof(TimeSpan)) - { - // Null reference exception will be handled upstream - return TimeSpan.Parse(rawValue!, _formatProvider); - } - - // Enum - if (targetType.IsEnum) - { - // Null reference exception will be handled upstream - return Enum.Parse(targetType, rawValue!, true); - } - - // Convertible primitives (int, double, char, etc) - if (targetType.Implements(typeof(IConvertible))) - { - return Convert.ChangeType(rawValue, targetType, _formatProvider); - } - - // Nullable - var nullableUnderlyingType = targetType.TryGetNullableUnderlyingType(); - if (nullableUnderlyingType is not null) - { - return !string.IsNullOrWhiteSpace(rawValue) - ? ConvertSingle(inputSchema, rawValue, nullableUnderlyingType) - : null; - } - - // String-constructable (FileInfo, etc) - var stringConstructor = targetType.GetConstructor([typeof(string)]); - if (stringConstructor is not null) - { - return stringConstructor.Invoke([rawValue]); - } - - // String-parseable (with IFormatProvider) - var parseMethodWithFormatProvider = targetType.TryGetStaticParseMethod(true); - if (parseMethodWithFormatProvider is not null) - { - return parseMethodWithFormatProvider.Invoke(null, [rawValue, _formatProvider]); - } - - // String-parseable (without IFormatProvider) - var parseMethod = targetType.TryGetStaticParseMethod(); - if (parseMethod is not null) - { - return parseMethod.Invoke(null, [rawValue]); - } - - throw CliFxException.InternalError( - $""" - {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} has an unsupported underlying property type. - There is no known way to convert a string value into an instance of type `{targetType.FullName}`. - To fix this, either change the property to use a supported type or configure a custom converter. - """ - ); - } - - private object? ConvertMultiple( - InputSchema inputSchema, - IReadOnlyList rawValues, - Type targetEnumerableType, - Type targetElementType - ) - { - var array = rawValues - .Select(v => ConvertSingle(inputSchema, v, targetElementType)) - .ToNonGenericArray(targetElementType); - - var arrayType = array.GetType(); - - // Assignable from an array (T[], IReadOnlyList, etc) - if (targetEnumerableType.IsAssignableFrom(arrayType)) - { - return array; - } - - // Array-constructable (List, HashSet, etc) - var arrayConstructor = targetEnumerableType.GetConstructor([arrayType]); - if (arrayConstructor is not null) - { - return arrayConstructor.Invoke([array]); - } - - throw CliFxException.InternalError( - $""" - {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} has an unsupported underlying property type. - There is no known way to convert an array of `{targetElementType.FullName}` into an instance of type `{targetEnumerableType.FullName}`. - To fix this, change the property to use a type which can be assigned from an array or a type which has a constructor that accepts an array. - """ - ); - } - - private object? ConvertMember(InputSchema inputSchema, IReadOnlyList rawValues) - { - try - { - // Non-scalar - var enumerableUnderlyingType = - inputSchema.Property.Type.TryGetEnumerableUnderlyingType(); - - if (enumerableUnderlyingType is not null && inputSchema.Property.Type != typeof(string)) - { - return ConvertMultiple( - inputSchema, - rawValues, - inputSchema.Property.Type, - enumerableUnderlyingType - ); - } - - // Scalar - if (rawValues.Count <= 1) - { - return ConvertSingle( - inputSchema, - rawValues.SingleOrDefault(), - inputSchema.Property.Type - ); - } - } - catch (Exception ex) when (ex is not CliFxException) // don't wrap CliFxException - { - // We use reflection-based invocation which can throw TargetInvocationException. - // Unwrap those exceptions to provide a more user-friendly error message. - var errorMessage = ex is TargetInvocationException invokeEx - ? invokeEx.InnerException?.Message ?? invokeEx.Message - : ex.Message; - - throw CliFxException.UserError( - $""" - {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} cannot be set from the provided argument(s): - {rawValues.Select(v => '<' + v + '>').JoinToString(" ")} - Error: {errorMessage} - """, - ex - ); - } - - // Mismatch (scalar but too many values) - throw CliFxException.UserError( - $""" - {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple: - {rawValues.Select(v => '<' + v + '>').JoinToString(" ")} - """ - ); - } - - private void ValidateMember(InputSchema inputSchema, object? convertedValue) - { - var errors = new List(); - - foreach (var validatorType in inputSchema.Validators) - { - var validator = typeActivator.CreateInstance(validatorType); - var error = validator.Validate(convertedValue); - - if (error is not null) - errors.Add(error); - } - - if (errors.Any()) - { - throw CliFxException.UserError( - $""" - {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} has been provided with an invalid value. - Error(s): - {errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)} - """ - ); - } - } - - private void BindMember( - InputSchema inputSchema, - ICommand commandInstance, - IReadOnlyList rawValues - ) - { - var convertedValue = ConvertMember(inputSchema, rawValues); - ValidateMember(inputSchema, convertedValue); - - inputSchema.Property.SetValue(commandInstance, convertedValue); - } - - private void BindParameters( - CommandInput commandInput, - CommandSchema commandSchema, - ICommand commandInstance - ) - { - // Ensure there are no unexpected parameters and that all parameters are provided - var remainingParameterInputs = commandInput.Parameters.ToList(); - var remainingRequiredParameterSchemas = commandSchema - .Parameters.Where(p => p.IsRequired) - .ToList(); - - var position = 0; - - foreach (var parameterSchema in commandSchema.Parameters.OrderBy(p => p.Order)) - { - // Break when there are no remaining inputs - if (position >= commandInput.Parameters.Count) - break; - - // Scalar: take one input at the current position - if (parameterSchema.Property.IsScalar()) - { - var parameterInput = commandInput.Parameters[position]; - BindMember(parameterSchema, commandInstance, [parameterInput.Value]); - - position++; - remainingParameterInputs.Remove(parameterInput); - } - // Non-scalar: take all remaining inputs starting from the current position - else - { - var parameterInputs = commandInput.Parameters.Skip(position).ToArray(); - - BindMember( - parameterSchema, - commandInstance, - parameterInputs.Select(p => p.Value).ToArray() - ); - - position += parameterInputs.Length; - remainingParameterInputs.RemoveRange(parameterInputs); - } - - remainingRequiredParameterSchemas.Remove(parameterSchema); - } - - if (remainingParameterInputs.Any()) - { - throw CliFxException.UserError( - $""" - Unexpected parameter(s): - {remainingParameterInputs.Select(p => p.GetFormattedIdentifier()).JoinToString(" ")} - """ - ); - } - - if (remainingRequiredParameterSchemas.Any()) - { - throw CliFxException.UserError( - $""" - Missing required parameter(s): - {remainingRequiredParameterSchemas - .Select(p => p.GetFormattedIdentifier()) - .JoinToString(" ")} - """ - ); - } - } - - private void BindOptions( - CommandInput commandInput, - CommandSchema commandSchema, - ICommand commandInstance - ) - { - // Ensure there are no unrecognized options and that all required options are set - var remainingOptionInputs = commandInput.Options.ToList(); - var remainingRequiredOptionSchemas = commandSchema - .Options.Where(o => o.IsRequired) - .ToList(); - - foreach (var optionSchema in commandSchema.Options) - { - var optionInputs = commandInput - .Options.Where(o => optionSchema.MatchesIdentifier(o.Identifier)) - .ToArray(); - - var environmentVariableInput = commandInput.EnvironmentVariables.FirstOrDefault(e => - optionSchema.MatchesEnvironmentVariable(e.Name) - ); - - // Direct input - if (optionInputs.Any()) - { - var rawValues = optionInputs.SelectMany(o => o.Values).ToArray(); - - BindMember(optionSchema, commandInstance, rawValues); - - // Required options need at least one value to be set - if (rawValues.Any()) - remainingRequiredOptionSchemas.Remove(optionSchema); - } - // Environment variable - else if (environmentVariableInput is not null) - { - var rawValues = optionSchema.IsSequence - ? [environmentVariableInput.Value] - : environmentVariableInput.SplitValues(); - - BindMember(optionSchema, commandInstance, rawValues); - - // Required options need at least one value to be set - if (rawValues.Any()) - remainingRequiredOptionSchemas.Remove(optionSchema); - } - // No input, skip - else - { - continue; - } - - remainingOptionInputs.RemoveRange(optionInputs); - } - - if (remainingOptionInputs.Any()) - { - throw CliFxException.UserError( - $""" - Unrecognized option(s): - {remainingOptionInputs.Select(o => o.GetFormattedIdentifier()).JoinToString(", ")} - """ - ); - } - - if (remainingRequiredOptionSchemas.Any()) - { - throw CliFxException.UserError( - $""" - Missing required option(s): - {remainingRequiredOptionSchemas - .Select(o => o.GetFormattedIdentifier()) - .JoinToString(", ")} - """ - ); - } - } - - public void Bind( - CommandInput commandInput, - CommandSchema commandSchema, - ICommand commandInstance - ) - { - BindParameters(commandInput, commandSchema, commandInstance); - BindOptions(commandInput, commandSchema, commandInstance); - } -} diff --git a/CliFx/Formatting/HelpConsoleFormatter.cs b/CliFx/Formatting/HelpConsoleFormatter.cs index a66f568..16aaf92 100644 --- a/CliFx/Formatting/HelpConsoleFormatter.cs +++ b/CliFx/Formatting/HelpConsoleFormatter.cs @@ -308,50 +308,49 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con private void WriteDefaultValue(InputSchema schema) { var defaultValue = context.CommandDefaultValues.GetValueOrDefault(schema); - if (defaultValue is not null) + if (defaultValue is null) return; + + // Non-Scalar + if (defaultValue is not string && defaultValue is IEnumerable defaultValues) { - // Non-Scalar - if (defaultValue is not string && defaultValue is IEnumerable defaultValues) + var elementType = + schema.Property.Type.TryGetEnumerableUnderlyingType() ?? typeof(object); + + if (elementType.IsToStringOverriden()) { - var elementType = - defaultValues.GetType().TryGetEnumerableUnderlyingType() ?? typeof(object); + Write(ConsoleColor.White, "Default: "); - if (elementType.IsToStringOverriden()) + var isFirst = true; + + foreach (var element in defaultValues) { - Write(ConsoleColor.White, "Default: "); - - var isFirst = true; - - foreach (var element in defaultValues) + if (isFirst) { - if (isFirst) - { - isFirst = false; - } - else - { - Write(", "); - } - - Write('"'); - Write(element.ToString(CultureInfo.InvariantCulture)); - Write('"'); + isFirst = false; + } + else + { + Write(", "); } - Write('.'); + Write('"'); + Write(element.ToString(CultureInfo.InvariantCulture)); + Write('"'); } - } - else - { - if (defaultValue.GetType().IsToStringOverriden()) - { - Write(ConsoleColor.White, "Default: "); - Write('"'); - Write(defaultValue.ToString(CultureInfo.InvariantCulture)); - Write('"'); - Write('.'); - } + Write('.'); + } + } + else + { + if (schema.Property.Type.IsToStringOverriden()) + { + Write(ConsoleColor.White, "Default: "); + + Write('"'); + Write(defaultValue.ToString(CultureInfo.InvariantCulture)); + Write('"'); + Write('.'); } } } diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs index 11bf30f..1aa10d2 100644 --- a/CliFx/ICommand.cs +++ b/CliFx/ICommand.cs @@ -1,5 +1,7 @@ using System.Threading.Tasks; using CliFx.Infrastructure; +using CliFx.Input; +using CliFx.Schema; namespace CliFx; @@ -8,6 +10,15 @@ namespace CliFx; /// public interface ICommand { + /// + /// Binds the command input to the current instance, using the provided schema. + /// + /// + /// This method is implemented automatically by the framework and should not be + /// called directly. + /// + void Bind(CommandSchema schema, CommandInput input); + /// /// Executes the command using the specified implementation of . /// diff --git a/CliFx/ICommandWithVersionOption.cs b/CliFx/ICommandWithVersionOption.cs index 3ad23d8..48134ee 100644 --- a/CliFx/ICommandWithVersionOption.cs +++ b/CliFx/ICommandWithVersionOption.cs @@ -6,7 +6,7 @@ public interface ICommandWithVersionOption : ICommand { /// - /// Whether the user requested the version information (via the `--version` option). + /// Whether the user requested the application version information (via the `--version` option). /// bool IsVersionRequested { get; } } diff --git a/CliFx/Infrastructure/SystemConsole.cs b/CliFx/Infrastructure/SystemConsole.cs index f696637..1f8e955 100644 --- a/CliFx/Infrastructure/SystemConsole.cs +++ b/CliFx/Infrastructure/SystemConsole.cs @@ -56,14 +56,18 @@ public class SystemConsole : IConsole, IDisposable public int WindowWidth { get => Console.WindowWidth; +#pragma warning disable CA1416 set => Console.WindowWidth = value; +#pragma warning restore CA1416 } /// public int WindowHeight { get => Console.WindowHeight; +#pragma warning disable CA1416 set => Console.WindowHeight = value; +#pragma warning restore CA1416 } /// diff --git a/CliFx/Input/CommandInput.cs b/CliFx/Input/CommandInput.cs index 74b6bdc..796515d 100644 --- a/CliFx/Input/CommandInput.cs +++ b/CliFx/Input/CommandInput.cs @@ -5,7 +5,10 @@ using CliFx.Utils.Extensions; namespace CliFx.Input; -internal partial class CommandInput( +/// +/// Describes input for a command. +/// +public partial class CommandInput( string? commandName, IReadOnlyList directives, IReadOnlyList parameters, diff --git a/CliFx/Input/DirectiveInput.cs b/CliFx/Input/DirectiveInput.cs index bb12a02..12d318a 100644 --- a/CliFx/Input/DirectiveInput.cs +++ b/CliFx/Input/DirectiveInput.cs @@ -2,7 +2,7 @@ namespace CliFx.Input; -internal class DirectiveInput(string name) +public class DirectiveInput(string name) { public string Name { get; } = name; diff --git a/CliFx/Input/EnvironmentVariableInput.cs b/CliFx/Input/EnvironmentVariableInput.cs index 988599f..4ebd59f 100644 --- a/CliFx/Input/EnvironmentVariableInput.cs +++ b/CliFx/Input/EnvironmentVariableInput.cs @@ -3,7 +3,7 @@ using System.IO; namespace CliFx.Input; -internal class EnvironmentVariableInput(string name, string value) +public class EnvironmentVariableInput(string name, string value) { public string Name { get; } = name; diff --git a/CliFx/Input/OptionInput.cs b/CliFx/Input/OptionInput.cs index 74c4d42..327a19d 100644 --- a/CliFx/Input/OptionInput.cs +++ b/CliFx/Input/OptionInput.cs @@ -2,13 +2,22 @@ namespace CliFx.Input; -internal class OptionInput(string identifier, IReadOnlyList values) +/// +/// Describes the materialized input for an option of a command. +/// +public class OptionInput(string identifier, IReadOnlyList values) { + /// + /// Option identifier (either the name or the short name). + /// public string Identifier { get; } = identifier; + /// + /// Provided option values. + /// public IReadOnlyList Values { get; } = values; - public string GetFormattedIdentifier() => + internal string GetFormattedIdentifier() => Identifier switch { { Length: >= 2 } => "--" + Identifier, diff --git a/CliFx/Input/ParameterInput.cs b/CliFx/Input/ParameterInput.cs index 6b4d590..d533859 100644 --- a/CliFx/Input/ParameterInput.cs +++ b/CliFx/Input/ParameterInput.cs @@ -1,8 +1,14 @@ namespace CliFx.Input; -internal class ParameterInput(string value) +/// +/// Describes the materialized input for a parameter of a command. +/// +public class ParameterInput(string value) { + /// + /// Parameter value. + /// public string Value { get; } = value; - public string GetFormattedIdentifier() => $"<{Value}>"; + internal string GetFormattedIdentifier() => $"<{Value}>"; } diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs index 1715582..68df727 100644 --- a/CliFx/Schema/CommandSchema.cs +++ b/CliFx/Schema/CommandSchema.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace CliFx.Schema; /// -/// Describes an individual command. +/// Describes an individual command, with its parameter and option bindings. /// public class CommandSchema( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, diff --git a/CliFx/Schema/InputSchema.cs b/CliFx/Schema/InputSchema.cs index fe57764..0401641 100644 --- a/CliFx/Schema/InputSchema.cs +++ b/CliFx/Schema/InputSchema.cs @@ -4,7 +4,7 @@ using CliFx.Extensibility; namespace CliFx.Schema; /// -/// Describes an input of a command, which can be either a parameter or an option. +/// Describes an input binding of a command. /// public abstract class InputSchema( PropertyBinding property, diff --git a/CliFx/Schema/OptionSchema.cs b/CliFx/Schema/OptionSchema.cs index 493760e..c59dce2 100644 --- a/CliFx/Schema/OptionSchema.cs +++ b/CliFx/Schema/OptionSchema.cs @@ -6,7 +6,7 @@ using CliFx.Extensibility; namespace CliFx.Schema; /// -/// Describes an option input of a command. +/// Describes an option binding of a command. /// public class OptionSchema( PropertyBinding property, diff --git a/CliFx/Schema/ParameterSchema.cs b/CliFx/Schema/ParameterSchema.cs index b52f40d..15f75db 100644 --- a/CliFx/Schema/ParameterSchema.cs +++ b/CliFx/Schema/ParameterSchema.cs @@ -4,7 +4,7 @@ using CliFx.Extensibility; namespace CliFx.Schema; /// -/// Describes a parameter input of a command. +/// Describes a parameter binding of a command. /// public class ParameterSchema( PropertyBinding property, diff --git a/CliFx/Schema/PropertyBinding.cs b/CliFx/Schema/PropertyBinding.cs index e8d901d..a7a48dc 100644 --- a/CliFx/Schema/PropertyBinding.cs +++ b/CliFx/Schema/PropertyBinding.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace CliFx.Schema; /// -/// Represents a binding to a CLR property. +/// Represents a CLR property binding. /// public class PropertyBinding( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods)] Type type, Func getValue, Action setValue @@ -16,6 +18,7 @@ public class PropertyBinding( /// /// Underlying CLR type of the property. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods)] public Type Type { get; } = type; ///