From cad1c1447441867ffd330153e7e6c4e4c85b80e5 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Tue, 28 May 2024 21:20:09 +0300 Subject: [PATCH] asd --- .../CliFx.Analyzers.Tests.csproj | 6 +- CliFx.Analyzers/CliFx.Analyzers.csproj | 2 +- CliFx.Tests/CliFx.Tests.csproj | 8 +- CliFx/ApplicationConfiguration.cs | 9 +- CliFx/CliApplication.cs | 16 ++- CliFx/CliApplicationBuilder.cs | 108 ++++------------ CliFx/CliFx.csproj | 2 +- CliFx/CommandBinder.cs | 53 ++++---- CliFx/Extensibility/BindingConverter.cs | 14 ++- CliFx/Extensibility/BindingValidator.cs | 13 +- CliFx/FallbackDefaultCommand.cs | 11 +- CliFx/Formatting/HelpContext.cs | 4 +- CliFx/Infrastructure/DefaultTypeActivator.cs | 5 +- CliFx/Infrastructure/DelegateTypeActivator.cs | 5 +- CliFx/Infrastructure/ITypeActivator.cs | 10 +- CliFx/Input/CommandInput.cs | 4 - CliFx/Input/OptionInput.cs | 5 - CliFx/Schema/ApplicationSchema.cs | 38 +++--- CliFx/Schema/BindablePropertyDescriptor.cs | 40 ------ CliFx/Schema/CommandSchema.cs | 119 +++++------------- CliFx/Schema/IInputSchema.cs | 36 ++++++ CliFx/Schema/IMemberSchema.cs | 26 ---- CliFx/Schema/IPropertyDescriptor.cs | 23 ---- CliFx/Schema/NullPropertyDescriptor.cs | 20 --- CliFx/Schema/OptionSchema.cs | 111 ++++++---------- CliFx/Schema/ParameterSchema.cs | 68 +++++----- CliFx/Schema/PropertyDescriptor.cs | 31 +++++ 27 files changed, 300 insertions(+), 487 deletions(-) delete mode 100644 CliFx/Schema/BindablePropertyDescriptor.cs create mode 100644 CliFx/Schema/IInputSchema.cs delete mode 100644 CliFx/Schema/IMemberSchema.cs delete mode 100644 CliFx/Schema/IPropertyDescriptor.cs delete mode 100644 CliFx/Schema/NullPropertyDescriptor.cs create mode 100644 CliFx/Schema/PropertyDescriptor.cs diff --git a/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj b/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj index bbb4ed3..5520438 100644 --- a/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj +++ b/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj @@ -14,10 +14,10 @@ - + - - + + diff --git a/CliFx.Analyzers/CliFx.Analyzers.csproj b/CliFx.Analyzers/CliFx.Analyzers.csproj index 72d720a..10b7043 100644 --- a/CliFx.Analyzers/CliFx.Analyzers.csproj +++ b/CliFx.Analyzers/CliFx.Analyzers.csproj @@ -21,7 +21,7 @@ - + \ No newline at end of file diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj index 53d6aa5..d1ea433 100644 --- a/CliFx.Tests/CliFx.Tests.csproj +++ b/CliFx.Tests/CliFx.Tests.csproj @@ -17,10 +17,10 @@ - - - - + + + + diff --git a/CliFx/ApplicationConfiguration.cs b/CliFx/ApplicationConfiguration.cs index 5aa9842..ea464c2 100644 --- a/CliFx/ApplicationConfiguration.cs +++ b/CliFx/ApplicationConfiguration.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using CliFx.Schema; namespace CliFx; @@ -7,15 +6,15 @@ namespace CliFx; /// Configuration of an application. /// public class ApplicationConfiguration( - IReadOnlyList commandTypes, + ApplicationSchema schema, bool isDebugModeAllowed, bool isPreviewModeAllowed ) { /// - /// Command types defined in the application. + /// Application schema. /// - public IReadOnlyList CommandTypes { get; } = commandTypes; + public ApplicationSchema Schema { get; } = schema; /// /// Whether debug mode is allowed in the application. diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 945feef..a67d821 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; using CliFx.Exceptions; using CliFx.Formatting; @@ -175,15 +174,14 @@ public class CliApplication( { try { - var applicationSchema = ApplicationSchema.Resolve(Configuration.CommandTypes); - - var commandInput = CommandInput.Parse( - commandLineArguments, - environmentVariables, - applicationSchema.GetCommandNames() + return await RunAsync( + Configuration.Schema, + CommandInput.Parse( + commandLineArguments, + environmentVariables, + Configuration.Schema.GetCommandNames() + ) ); - - return await RunAsync(applicationSchema, commandInput); } // To prevent the app from showing the annoying troubleshooting dialog on Windows, // we handle all exceptions ourselves and print them to the console. diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index c8d90b9..107a84f 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Reflection; -using CliFx.Attributes; using CliFx.Infrastructure; using CliFx.Schema; using CliFx.Utils; @@ -16,7 +15,7 @@ namespace CliFx; /// public partial class CliApplicationBuilder { - private readonly HashSet _commandTypes = []; + private readonly HashSet _commandSchemas = []; private bool _isDebugModeAllowed = true; private bool _isPreviewModeAllowed = true; @@ -30,71 +29,12 @@ public partial class CliApplicationBuilder /// /// Adds a command to the application. /// - public CliApplicationBuilder AddCommand(Type commandType) + public CliApplicationBuilder AddCommand(CommandSchema commandSchema) { - _commandTypes.Add(commandType); + _commandSchemas.Add(commandSchema); return this; } - /// - /// Adds a command to the application. - /// - public CliApplicationBuilder AddCommand() - where TCommand : ICommand => AddCommand(typeof(TCommand)); - - /// - /// Adds multiple commands to the application. - /// - public CliApplicationBuilder AddCommands(IEnumerable commandTypes) - { - foreach (var commandType in commandTypes) - AddCommand(commandType); - - return this; - } - - /// - /// Adds commands from the specified assembly to the application. - /// - /// - /// This method looks for public non-abstract classes that implement - /// and are annotated by . - /// - public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly) - { - foreach ( - var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType) - ) - AddCommand(commandType); - - return this; - } - - /// - /// Adds commands from the specified assemblies to the application. - /// - /// - /// This method looks for public non-abstract classes that implement - /// and are annotated by . - /// - public CliApplicationBuilder AddCommandsFrom(IEnumerable commandAssemblies) - { - foreach (var commandAssembly in commandAssemblies) - AddCommandsFrom(commandAssembly); - - return this; - } - - /// - /// Adds commands from the calling assembly to the application. - /// - /// - /// This method looks for public non-abstract classes that implement - /// and are annotated by . - /// - public CliApplicationBuilder AddCommandsFromThisAssembly() => - AddCommandsFrom(Assembly.GetCallingAssembly()); - /// /// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application. /// @@ -189,15 +129,6 @@ public partial class CliApplicationBuilder // Null returns are handled by DelegateTypeActivator UseTypeActivator(serviceProvider.GetService!); - /// - /// Configures the application to use the specified service provider for activating types. - /// This method takes a delegate that receives the list of all added command types, so that you can - /// easily register them with the service provider. - /// - public CliApplicationBuilder UseTypeActivator( - Func, IServiceProvider> getServiceProvider - ) => UseTypeActivator(getServiceProvider(_commandTypes.ToArray())); - /// /// Creates a configured instance of . /// @@ -211,7 +142,7 @@ public partial class CliApplicationBuilder ); var configuration = new ApplicationConfiguration( - _commandTypes.ToArray(), + new ApplicationSchema(_commandSchemas.ToArray()), _isDebugModeAllowed, _isPreviewModeAllowed ); @@ -241,15 +172,17 @@ public partial class CliApplicationBuilder return entryAssemblyName; } + [UnconditionalSuppressMessage( + "SingleFile", + "IL3000:Avoid accessing Assembly file path when publishing as a single file", + Justification = "The return value of the method is checked to ensure the assembly location is available." + )] private static string GetDefaultExecutableName() { - var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location; var processFilePath = EnvironmentEx.ProcessPath; - if ( - string.IsNullOrWhiteSpace(entryAssemblyFilePath) - || string.IsNullOrWhiteSpace(processFilePath) - ) + // Process file path should generally always be available + if (string.IsNullOrWhiteSpace(processFilePath)) { throw new InvalidOperationException( "Failed to infer the default application executable name. " @@ -257,15 +190,22 @@ public partial class CliApplicationBuilder ); } - // If the process path matches the entry assembly path, it's a legacy .NET Framework app - // or a self-contained .NET Core app. + var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location; + + // Single file application: entry assembly is not on disk and doesn't have a file path + if (string.IsNullOrWhiteSpace(entryAssemblyFilePath)) + { + return Path.GetFileNameWithoutExtension(processFilePath); + } + + // Legacy .NET Framework application: entry assembly has the same file path as the process if (PathEx.AreEqual(entryAssemblyFilePath, processFilePath)) { return Path.GetFileNameWithoutExtension(entryAssemblyFilePath); } - // If the process path has the same name and parent directory as the entry assembly path, - // but different extension, it's a framework-dependent .NET Core app launched through the apphost. + // .NET Core application launched through the native application host: + // entry assembly has the same file path as the process, but with a different extension. if ( PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, "exe"), processFilePath) || PathEx.AreEqual( @@ -277,7 +217,7 @@ public partial class CliApplicationBuilder return Path.GetFileNameWithoutExtension(entryAssemblyFilePath); } - // Otherwise, it's a framework-dependent .NET Core app launched through the .NET CLI + // .NET Core application launched through the .NET CLI return "dotnet " + Path.GetFileName(entryAssemblyFilePath); } diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index 8fa2e0b..67fbfe4 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -25,7 +25,7 @@ - + diff --git a/CliFx/CommandBinder.cs b/CliFx/CommandBinder.cs index 5ea4dba..a815626 100644 --- a/CliFx/CommandBinder.cs +++ b/CliFx/CommandBinder.cs @@ -16,14 +16,12 @@ internal class CommandBinder(ITypeActivator typeActivator) { private readonly IFormatProvider _formatProvider = CultureInfo.InvariantCulture; - private object? ConvertSingle(IMemberSchema memberSchema, string? rawValue, Type targetType) + private object? ConvertSingle(IInputSchema inputSchema, string? rawValue, Type targetType) { // Custom converter - if (memberSchema.ConverterType is not null) + if (inputSchema.Converter is not null) { - var converter = typeActivator.CreateInstance( - memberSchema.ConverterType - ); + var converter = typeActivator.CreateInstance(inputSchema.Converter); return converter.Convert(rawValue); } @@ -71,7 +69,7 @@ internal class CommandBinder(ITypeActivator typeActivator) if (nullableUnderlyingType is not null) { return !string.IsNullOrWhiteSpace(rawValue) - ? ConvertSingle(memberSchema, rawValue, nullableUnderlyingType) + ? ConvertSingle(inputSchema, rawValue, nullableUnderlyingType) : null; } @@ -98,7 +96,7 @@ internal class CommandBinder(ITypeActivator typeActivator) throw CliFxException.InternalError( $""" - {memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type. + {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. """ @@ -106,14 +104,14 @@ internal class CommandBinder(ITypeActivator typeActivator) } private object? ConvertMultiple( - IMemberSchema memberSchema, + IInputSchema inputSchema, IReadOnlyList rawValues, Type targetEnumerableType, Type targetElementType ) { var array = rawValues - .Select(v => ConvertSingle(memberSchema, v, targetElementType)) + .Select(v => ConvertSingle(inputSchema, v, targetElementType)) .ToNonGenericArray(targetElementType); var arrayType = array.GetType(); @@ -133,30 +131,27 @@ internal class CommandBinder(ITypeActivator typeActivator) throw CliFxException.InternalError( $""" - {memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has an unsupported underlying property type. + {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(IMemberSchema memberSchema, IReadOnlyList rawValues) + private object? ConvertMember(IInputSchema inputSchema, IReadOnlyList rawValues) { try { // Non-scalar var enumerableUnderlyingType = - memberSchema.Property.Type.TryGetEnumerableUnderlyingType(); + inputSchema.Property.Type.TryGetEnumerableUnderlyingType(); - if ( - enumerableUnderlyingType is not null - && memberSchema.Property.Type != typeof(string) - ) + if (enumerableUnderlyingType is not null && inputSchema.Property.Type != typeof(string)) { return ConvertMultiple( - memberSchema, + inputSchema, rawValues, - memberSchema.Property.Type, + inputSchema.Property.Type, enumerableUnderlyingType ); } @@ -165,9 +160,9 @@ internal class CommandBinder(ITypeActivator typeActivator) if (rawValues.Count <= 1) { return ConvertSingle( - memberSchema, + inputSchema, rawValues.SingleOrDefault(), - memberSchema.Property.Type + inputSchema.Property.Type ); } } @@ -181,7 +176,7 @@ internal class CommandBinder(ITypeActivator typeActivator) throw CliFxException.UserError( $""" - {memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from the provided argument(s): + {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} cannot be set from the provided argument(s): {rawValues.Select(v => '<' + v + '>').JoinToString(" ")} Error: {errorMessage} """, @@ -192,17 +187,17 @@ internal class CommandBinder(ITypeActivator typeActivator) // Mismatch (scalar but too many values) throw CliFxException.UserError( $""" - {memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple: + {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple: {rawValues.Select(v => '<' + v + '>').JoinToString(" ")} """ ); } - private void ValidateMember(IMemberSchema memberSchema, object? convertedValue) + private void ValidateMember(IInputSchema inputSchema, object? convertedValue) { var errors = new List(); - foreach (var validatorType in memberSchema.ValidatorTypes) + foreach (var validatorType in inputSchema.Validators) { var validator = typeActivator.CreateInstance(validatorType); var error = validator.Validate(convertedValue); @@ -215,7 +210,7 @@ internal class CommandBinder(ITypeActivator typeActivator) { throw CliFxException.UserError( $""" - {memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has been provided with an invalid value. + {inputSchema.GetKind()} {inputSchema.GetFormattedIdentifier()} has been provided with an invalid value. Error(s): {errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)} """ @@ -224,15 +219,15 @@ internal class CommandBinder(ITypeActivator typeActivator) } private void BindMember( - IMemberSchema memberSchema, + IInputSchema inputSchema, ICommand commandInstance, IReadOnlyList rawValues ) { - var convertedValue = ConvertMember(memberSchema, rawValues); - ValidateMember(memberSchema, convertedValue); + var convertedValue = ConvertMember(inputSchema, rawValues); + ValidateMember(inputSchema, convertedValue); - memberSchema.Property.SetValue(commandInstance, convertedValue); + inputSchema.Property.SetValue(commandInstance, convertedValue); } private void BindParameters( diff --git a/CliFx/Extensibility/BindingConverter.cs b/CliFx/Extensibility/BindingConverter.cs index b0b8a97..ae332aa 100644 --- a/CliFx/Extensibility/BindingConverter.cs +++ b/CliFx/Extensibility/BindingConverter.cs @@ -1,8 +1,16 @@ namespace CliFx.Extensibility; -// Used internally to simplify the usage from reflection -internal interface IBindingConverter +/// +/// Defines a custom conversion for binding command-line arguments to command inputs. +/// +/// +/// To implement your own converter, inherit from instead. +/// +public interface IBindingConverter { + /// + /// Parses the value from a raw command-line argument. + /// object? Convert(string? rawValue); } @@ -12,7 +20,7 @@ internal interface IBindingConverter public abstract class BindingConverter : IBindingConverter { /// - /// Parses value from a raw command-line argument. + /// Parses the value from a raw command-line argument. /// public abstract T Convert(string? rawValue); diff --git a/CliFx/Extensibility/BindingValidator.cs b/CliFx/Extensibility/BindingValidator.cs index 9b3e394..a87562d 100644 --- a/CliFx/Extensibility/BindingValidator.cs +++ b/CliFx/Extensibility/BindingValidator.cs @@ -1,8 +1,17 @@ namespace CliFx.Extensibility; -// Used internally to simplify the usage from reflection -internal interface IBindingValidator +/// +/// Defines a custom validation rules for values bound from command-line arguments. +/// +/// +/// To implement your own validator, inherit from instead. +/// +public interface IBindingValidator { + /// + /// Validates the value bound to a parameter or an option. + /// Returns null if validation is successful, or an error in case of failure. + /// BindingValidationError? Validate(object? value); } diff --git a/CliFx/FallbackDefaultCommand.cs b/CliFx/FallbackDefaultCommand.cs index c55a89e..ef175f1 100644 --- a/CliFx/FallbackDefaultCommand.cs +++ b/CliFx/FallbackDefaultCommand.cs @@ -9,12 +9,15 @@ namespace CliFx; // Fallback command used when the application doesn't have one configured. // This command is only used as a stub for help text. [Command] -internal class FallbackDefaultCommand : ICommand +internal partial class FallbackDefaultCommand : ICommand { - public static CommandSchema Schema { get; } = - CommandSchema.Resolve(typeof(FallbackDefaultCommand)); - // Never actually executed [ExcludeFromCodeCoverage] public ValueTask ExecuteAsync(IConsole console) => default; } + +internal partial class FallbackDefaultCommand +{ + public static CommandSchema Schema { get; } = + new(typeof(FallbackDefaultCommand), null, null, [], []); +} diff --git a/CliFx/Formatting/HelpContext.cs b/CliFx/Formatting/HelpContext.cs index f33319d..c898ec6 100644 --- a/CliFx/Formatting/HelpContext.cs +++ b/CliFx/Formatting/HelpContext.cs @@ -7,7 +7,7 @@ internal class HelpContext( ApplicationMetadata applicationMetadata, ApplicationSchema applicationSchema, CommandSchema commandSchema, - IReadOnlyDictionary commandDefaultValues + IReadOnlyDictionary commandDefaultValues ) { public ApplicationMetadata ApplicationMetadata { get; } = applicationMetadata; @@ -16,6 +16,6 @@ internal class HelpContext( public CommandSchema CommandSchema { get; } = commandSchema; - public IReadOnlyDictionary CommandDefaultValues { get; } = + public IReadOnlyDictionary CommandDefaultValues { get; } = commandDefaultValues; } diff --git a/CliFx/Infrastructure/DefaultTypeActivator.cs b/CliFx/Infrastructure/DefaultTypeActivator.cs index 78e5afb..5410f51 100644 --- a/CliFx/Infrastructure/DefaultTypeActivator.cs +++ b/CliFx/Infrastructure/DefaultTypeActivator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using CliFx.Exceptions; namespace CliFx.Infrastructure; @@ -10,7 +11,9 @@ namespace CliFx.Infrastructure; public class DefaultTypeActivator : ITypeActivator { /// - public object CreateInstance(Type type) + public object CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type + ) { try { diff --git a/CliFx/Infrastructure/DelegateTypeActivator.cs b/CliFx/Infrastructure/DelegateTypeActivator.cs index faf0987..772a460 100644 --- a/CliFx/Infrastructure/DelegateTypeActivator.cs +++ b/CliFx/Infrastructure/DelegateTypeActivator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using CliFx.Exceptions; namespace CliFx.Infrastructure; @@ -9,7 +10,9 @@ namespace CliFx.Infrastructure; public class DelegateTypeActivator(Func createInstance) : ITypeActivator { /// - public object CreateInstance(Type type) => + public object CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type + ) => createInstance(type) ?? throw CliFxException.InternalError( $""" diff --git a/CliFx/Infrastructure/ITypeActivator.cs b/CliFx/Infrastructure/ITypeActivator.cs index 8a17838..f3c55ae 100644 --- a/CliFx/Infrastructure/ITypeActivator.cs +++ b/CliFx/Infrastructure/ITypeActivator.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using CliFx.Exceptions; namespace CliFx.Infrastructure; @@ -11,12 +12,17 @@ public interface ITypeActivator /// /// Creates an instance of the specified type. /// - object CreateInstance(Type type); + object CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type + ); } internal static class TypeActivatorExtensions { - public static T CreateInstance(this ITypeActivator activator, Type type) + public static T CreateInstance( + this ITypeActivator activator, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type + ) { if (!typeof(T).IsAssignableFrom(type)) { diff --git a/CliFx/Input/CommandInput.cs b/CliFx/Input/CommandInput.cs index 408a06e..74b6bdc 100644 --- a/CliFx/Input/CommandInput.cs +++ b/CliFx/Input/CommandInput.cs @@ -33,10 +33,6 @@ internal partial class CommandInput( public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective); public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective); - - public bool IsHelpOptionSpecified => Options.Any(o => o.IsHelpOption); - - public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption); } internal partial class CommandInput diff --git a/CliFx/Input/OptionInput.cs b/CliFx/Input/OptionInput.cs index 257340a..74c4d42 100644 --- a/CliFx/Input/OptionInput.cs +++ b/CliFx/Input/OptionInput.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using CliFx.Schema; namespace CliFx.Input; @@ -9,10 +8,6 @@ internal class OptionInput(string identifier, IReadOnlyList values) public IReadOnlyList Values { get; } = values; - public bool IsHelpOption => OptionSchema.HelpOption.MatchesIdentifier(Identifier); - - public bool IsVersionOption => OptionSchema.VersionOption.MatchesIdentifier(Identifier); - public string GetFormattedIdentifier() => Identifier switch { diff --git a/CliFx/Schema/ApplicationSchema.cs b/CliFx/Schema/ApplicationSchema.cs index 54d5bd5..488ce9a 100644 --- a/CliFx/Schema/ApplicationSchema.cs +++ b/CliFx/Schema/ApplicationSchema.cs @@ -5,33 +5,39 @@ using CliFx.Utils.Extensions; namespace CliFx.Schema; -internal partial class ApplicationSchema(IReadOnlyList commands) +/// +/// Describes the structure of a command-line application. +/// +public class ApplicationSchema(IReadOnlyList commands) { + /// + /// Commands defined in the application. + /// public IReadOnlyList Commands { get; } = commands; - public IReadOnlyList GetCommandNames() => + internal IReadOnlyList GetCommandNames() => Commands.Select(c => c.Name).WhereNotNullOrWhiteSpace().ToArray(); - public CommandSchema? TryFindDefaultCommand() => Commands.FirstOrDefault(c => c.IsDefault); + internal CommandSchema? TryFindDefaultCommand() => Commands.FirstOrDefault(c => c.IsDefault); - public CommandSchema? TryFindCommand(string commandName) => + internal CommandSchema? TryFindCommand(string commandName) => Commands.FirstOrDefault(c => c.MatchesName(commandName)); private IReadOnlyList GetDescendantCommands( - IReadOnlyList potentialParentCommandSchemas, + IReadOnlyList potentialDescendantCommands, string? parentCommandName ) { var result = new List(); - foreach (var potentialParentCommandSchema in potentialParentCommandSchemas) + foreach (var potentialDescendantCommand in potentialDescendantCommands) { // Default commands can't be descendant of anything - if (string.IsNullOrWhiteSpace(potentialParentCommandSchema.Name)) + if (string.IsNullOrWhiteSpace(potentialDescendantCommand.Name)) continue; // Command can't be its own descendant - if (potentialParentCommandSchema.MatchesName(parentCommandName)) + if (potentialDescendantCommand.MatchesName(parentCommandName)) continue; var isDescendant = @@ -39,22 +45,22 @@ internal partial class ApplicationSchema(IReadOnlyList commands) string.IsNullOrWhiteSpace(parentCommandName) || // Otherwise a command is a descendant if it starts with the same name segments - potentialParentCommandSchema.Name.StartsWith( + potentialDescendantCommand.Name.StartsWith( parentCommandName + ' ', StringComparison.OrdinalIgnoreCase ); if (isDescendant) - result.Add(potentialParentCommandSchema); + result.Add(potentialDescendantCommand); } return result; } - public IReadOnlyList GetDescendantCommands(string? parentCommandName) => + internal IReadOnlyList GetDescendantCommands(string? parentCommandName) => GetDescendantCommands(Commands, parentCommandName); - public IReadOnlyList GetChildCommands(string? parentCommandName) + internal IReadOnlyList GetChildCommands(string? parentCommandName) { var descendants = GetDescendantCommands(parentCommandName); @@ -62,16 +68,8 @@ internal partial class ApplicationSchema(IReadOnlyList commands) // Filter out descendants of descendants, leave only direct children foreach (var descendant in descendants) - { result.RemoveRange(GetDescendantCommands(descendants, descendant.Name)); - } return result; } } - -internal partial class ApplicationSchema -{ - public static ApplicationSchema Resolve(IReadOnlyList commandTypes) => - new(commandTypes.Select(CommandSchema.Resolve).ToArray()); -} diff --git a/CliFx/Schema/BindablePropertyDescriptor.cs b/CliFx/Schema/BindablePropertyDescriptor.cs deleted file mode 100644 index 8f7e35d..0000000 --- a/CliFx/Schema/BindablePropertyDescriptor.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using CliFx.Utils.Extensions; - -namespace CliFx.Schema; - -internal class BindablePropertyDescriptor(PropertyInfo property) : IPropertyDescriptor -{ - public Type Type => property.PropertyType; - - public object? GetValue(ICommand commandInstance) => property.GetValue(commandInstance); - - public void SetValue(ICommand commandInstance, object? value) => - property.SetValue(commandInstance, value); - - public IReadOnlyList GetValidValues() - { - static Type GetUnderlyingType(Type type) - { - var enumerableUnderlyingType = type.TryGetEnumerableUnderlyingType(); - if (enumerableUnderlyingType is not null) - return GetUnderlyingType(enumerableUnderlyingType); - - var nullableUnderlyingType = type.TryGetNullableUnderlyingType(); - if (nullableUnderlyingType is not null) - return GetUnderlyingType(nullableUnderlyingType); - - return type; - } - - var underlyingType = GetUnderlyingType(Type); - - // We can only get valid values for enums - if (underlyingType.IsEnum) - return Enum.GetNames(underlyingType); - - return Array.Empty(); - } -} diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs index 0587984..5659737 100644 --- a/CliFx/Schema/CommandSchema.cs +++ b/CliFx/Schema/CommandSchema.cs @@ -1,45 +1,59 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using CliFx.Attributes; -using CliFx.Exceptions; -using CliFx.Utils.Extensions; +using System.Diagnostics.CodeAnalysis; namespace CliFx.Schema; -internal partial class CommandSchema( - Type type, +/// +/// Describes an individual command. +/// +public class CommandSchema( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, string? name, string? description, IReadOnlyList parameters, IReadOnlyList options ) { + /// + /// Command's CLR type. + /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] public Type Type { get; } = type; + /// + /// Command name. + /// public string? Name { get; } = name; + /// + /// Command description. + /// public string? Description { get; } = description; + /// + /// Command parameters. + /// public IReadOnlyList Parameters { get; } = parameters; + /// + /// Command options. + /// public IReadOnlyList Options { get; } = options; - public bool IsDefault => string.IsNullOrWhiteSpace(Name); + /// + /// Whether this command is the application's default command. + /// + public bool IsDefault { get; } = string.IsNullOrWhiteSpace(name); - public bool IsHelpOptionAvailable => Options.Contains(OptionSchema.HelpOption); - - public bool IsVersionOptionAvailable => Options.Contains(OptionSchema.VersionOption); - - public bool MatchesName(string? name) => + internal bool MatchesName(string? name) => !string.IsNullOrWhiteSpace(Name) ? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase) : string.IsNullOrWhiteSpace(name); - public IReadOnlyDictionary GetValues(ICommand instance) + internal IReadOnlyDictionary GetValues(ICommand instance) { - var result = new Dictionary(); + var result = new Dictionary(); foreach (var parameterSchema in Parameters) { @@ -56,78 +70,3 @@ internal partial class CommandSchema( return result; } } - -internal partial class CommandSchema -{ - public static bool IsCommandType(Type type) => - type.Implements(typeof(ICommand)) - && type.IsDefined(typeof(CommandAttribute)) - && type is { IsAbstract: false, IsInterface: false }; - - public static CommandSchema? TryResolve(Type type) - { - if (!IsCommandType(type)) - return null; - - var attribute = type.GetCustomAttribute(); - - var name = attribute?.Name?.Trim(); - var description = attribute?.Description?.Trim(); - - var implicitOptionSchemas = string.IsNullOrWhiteSpace(name) - ? new[] { OptionSchema.HelpOption, OptionSchema.VersionOption } - : new[] { OptionSchema.HelpOption }; - - var properties = type - // Get properties directly on the command type - .GetProperties() - // Get non-abstract properties on interfaces (to support default interfaces members) - .Union( - type.GetInterfaces() - // Only interfaces implementing ICommand for explicitness - .Where(i => i != typeof(ICommand) && i.IsAssignableTo(typeof(ICommand))) - .SelectMany(i => - i.GetProperties() - .Where(p => - p.GetMethod is not null - && !p.GetMethod.IsAbstract - && p.SetMethod is not null - && !p.SetMethod.IsAbstract - ) - ) - ) - .ToArray(); - - var parameterSchemas = properties - .Select(ParameterSchema.TryResolve) - .WhereNotNull() - .ToArray(); - - var optionSchemas = properties - .Select(OptionSchema.TryResolve) - .WhereNotNull() - .Concat(implicitOptionSchemas) - .ToArray(); - - return new CommandSchema(type, name, description, parameterSchemas, optionSchemas); - } - - public static CommandSchema Resolve(Type type) - { - var schema = TryResolve(type); - if (schema is null) - { - throw CliFxException.InternalError( - $""" - Type `{type.FullName}` is not a valid command type. - In order to be a valid command type, it must: - - Implement `{typeof(ICommand).FullName}` - - Be annotated with `{typeof(CommandAttribute).FullName}` - - Not be an abstract class - """ - ); - } - - return schema; - } -} diff --git a/CliFx/Schema/IInputSchema.cs b/CliFx/Schema/IInputSchema.cs new file mode 100644 index 0000000..790c690 --- /dev/null +++ b/CliFx/Schema/IInputSchema.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using CliFx.Extensibility; + +namespace CliFx.Schema; + +/// +/// Describes an input of a command, which can be either a parameter or an option. +/// +public interface IInputSchema +{ + /// + /// Information about the property that this input is bound to. + /// + PropertyDescriptor Property { get; } + + /// + /// Whether this input is a scalar (single value) or a sequence (multiple values). + /// + bool IsScalar { get; } + + /// + /// Valid values for this input, if applicable. + /// If the input does not have a predefined set of valid values, this property is null. + /// + IReadOnlyList? ValidValues { get; } + + /// + /// Optional binding converter for this input. + /// + IBindingConverter? Converter { get; } + + /// + /// Optional binding validator(s) for this input. + /// + IReadOnlyList Validators { get; } +} diff --git a/CliFx/Schema/IMemberSchema.cs b/CliFx/Schema/IMemberSchema.cs deleted file mode 100644 index 59919c9..0000000 --- a/CliFx/Schema/IMemberSchema.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace CliFx.Schema; - -internal interface IMemberSchema -{ - IPropertyDescriptor Property { get; } - - Type? ConverterType { get; } - - IReadOnlyList ValidatorTypes { get; } - - string GetFormattedIdentifier(); -} - -internal static class MemberSchemaExtensions -{ - public static string GetKind(this IMemberSchema memberSchema) => - memberSchema switch - { - ParameterSchema => "Parameter", - OptionSchema => "Option", - _ => throw new ArgumentOutOfRangeException(nameof(memberSchema)) - }; -} diff --git a/CliFx/Schema/IPropertyDescriptor.cs b/CliFx/Schema/IPropertyDescriptor.cs deleted file mode 100644 index a59beab..0000000 --- a/CliFx/Schema/IPropertyDescriptor.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using CliFx.Utils.Extensions; - -namespace CliFx.Schema; - -internal interface IPropertyDescriptor -{ - Type Type { get; } - - object? GetValue(ICommand commandInstance); - - void SetValue(ICommand commandInstance, object? value); - - IReadOnlyList GetValidValues(); -} - -internal static class PropertyDescriptorExtensions -{ - public static bool IsScalar(this IPropertyDescriptor propertyDescriptor) => - propertyDescriptor.Type == typeof(string) - || propertyDescriptor.Type.TryGetEnumerableUnderlyingType() is null; -} diff --git a/CliFx/Schema/NullPropertyDescriptor.cs b/CliFx/Schema/NullPropertyDescriptor.cs deleted file mode 100644 index e1c5257..0000000 --- a/CliFx/Schema/NullPropertyDescriptor.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace CliFx.Schema; - -internal partial class NullPropertyDescriptor : IPropertyDescriptor -{ - public Type Type { get; } = typeof(object); - - public object? GetValue(ICommand commandInstance) => null; - - public void SetValue(ICommand commandInstance, object? value) { } - - public IReadOnlyList GetValidValues() => Array.Empty(); -} - -internal partial class NullPropertyDescriptor -{ - public static NullPropertyDescriptor Instance { get; } = new(); -} diff --git a/CliFx/Schema/OptionSchema.cs b/CliFx/Schema/OptionSchema.cs index c169e1e..8638c8e 100644 --- a/CliFx/Schema/OptionSchema.cs +++ b/CliFx/Schema/OptionSchema.cs @@ -1,54 +1,79 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Text; -using CliFx.Attributes; -using CliFx.Utils.Extensions; +using CliFx.Extensibility; namespace CliFx.Schema; -internal partial class OptionSchema( - IPropertyDescriptor property, +/// +/// Describes a command's option. +/// +public class OptionSchema( + PropertyDescriptor property, string? name, char? shortName, string? environmentVariable, bool isRequired, string? description, - Type? converterType, - IReadOnlyList validatorTypes -) : IMemberSchema + IBindingConverter? converter, + IReadOnlyList validators +) : IInputSchema { - public IPropertyDescriptor Property { get; } = property; + /// + public PropertyDescriptor Property { get; } = property; + /// + public bool IsScalar { get; } + + /// + public IReadOnlyList? ValidValues { get; } + + /// + /// Option name. + /// public string? Name { get; } = name; + /// + /// Option short name. + /// public char? ShortName { get; } = shortName; + /// + /// Environment variable that can be used as a fallback for this option. + /// public string? EnvironmentVariable { get; } = environmentVariable; + /// + /// Whether the option is required. + /// public bool IsRequired { get; } = isRequired; + /// + /// Option description. + /// public string? Description { get; } = description; - public Type? ConverterType { get; } = converterType; + /// + public IBindingConverter? Converter { get; } = converter; - public IReadOnlyList ValidatorTypes { get; } = validatorTypes; + /// + public IReadOnlyList Validators { get; } = validators; - public bool MatchesName(string? name) => + internal bool MatchesName(string? name) => !string.IsNullOrWhiteSpace(Name) && string.Equals(Name, name, StringComparison.OrdinalIgnoreCase); - public bool MatchesShortName(char? shortName) => + internal bool MatchesShortName(char? shortName) => ShortName is not null && ShortName == shortName; - public bool MatchesIdentifier(string identifier) => + internal bool MatchesIdentifier(string identifier) => MatchesName(identifier) || identifier.Length == 1 && MatchesShortName(identifier[0]); - public bool MatchesEnvironmentVariable(string environmentVariableName) => + internal bool MatchesEnvironmentVariable(string environmentVariableName) => !string.IsNullOrWhiteSpace(EnvironmentVariable) && string.Equals(EnvironmentVariable, environmentVariableName, StringComparison.Ordinal); - public string GetFormattedIdentifier() + internal string GetFormattedIdentifier() { var buffer = new StringBuilder(); @@ -73,57 +98,3 @@ internal partial class OptionSchema( return buffer.ToString(); } } - -internal partial class OptionSchema -{ - public static OptionSchema? TryResolve(PropertyInfo property) - { - var attribute = property.GetCustomAttribute(); - if (attribute is null) - return null; - - // The user may mistakenly specify dashes, thinking it's required, so trim them - var name = attribute.Name?.TrimStart('-').Trim(); - var environmentVariable = attribute.EnvironmentVariable?.Trim(); - var isRequired = attribute.IsRequired || property.IsRequired(); - var description = attribute.Description?.Trim(); - - return new OptionSchema( - new BindablePropertyDescriptor(property), - name, - attribute.ShortName, - environmentVariable, - isRequired, - description, - attribute.Converter, - attribute.Validators - ); - } -} - -internal partial class OptionSchema -{ - public static OptionSchema HelpOption { get; } = - new( - NullPropertyDescriptor.Instance, - "help", - 'h', - null, - false, - "Shows help text.", - null, - Array.Empty() - ); - - public static OptionSchema VersionOption { get; } = - new( - NullPropertyDescriptor.Instance, - "version", - null, - null, - false, - "Shows version information.", - null, - Array.Empty() - ); -} diff --git a/CliFx/Schema/ParameterSchema.cs b/CliFx/Schema/ParameterSchema.cs index febf508..ce5dabe 100644 --- a/CliFx/Schema/ParameterSchema.cs +++ b/CliFx/Schema/ParameterSchema.cs @@ -1,58 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using CliFx.Attributes; -using CliFx.Utils.Extensions; +using System.Collections.Generic; +using CliFx.Extensibility; namespace CliFx.Schema; -internal partial class ParameterSchema( - IPropertyDescriptor property, +/// +/// Describes a command's parameter. +/// +public class ParameterSchema( + PropertyDescriptor property, int order, string name, bool isRequired, string? description, - Type? converterType, - IReadOnlyList validatorTypes -) : IMemberSchema + IBindingConverter? converter, + IReadOnlyList validators +) : IInputSchema { - public IPropertyDescriptor Property { get; } = property; + /// + public PropertyDescriptor Property { get; } = property; + /// + /// Order, in which the parameter is bound from the command-line arguments. + /// public int Order { get; } = order; + /// + /// Parameter name. + /// public string Name { get; } = name; + /// + /// Whether the parameter is required. + /// public bool IsRequired { get; } = isRequired; + /// + /// Parameter description. + /// public string? Description { get; } = description; - public Type? ConverterType { get; } = converterType; + /// + public IBindingConverter? Converter { get; } = converter; - public IReadOnlyList ValidatorTypes { get; } = validatorTypes; + /// + public IReadOnlyList Validators { get; } = validators; - public string GetFormattedIdentifier() => Property.IsScalar() ? $"<{Name}>" : $"<{Name}...>"; -} - -internal partial class ParameterSchema -{ - public static ParameterSchema? TryResolve(PropertyInfo property) - { - var attribute = property.GetCustomAttribute(); - if (attribute is null) - return null; - - var name = attribute.Name?.Trim() ?? property.Name.ToLowerInvariant(); - var isRequired = attribute.IsRequired || property.IsRequired(); - var description = attribute.Description?.Trim(); - - return new ParameterSchema( - new BindablePropertyDescriptor(property), - attribute.Order, - name, - isRequired, - description, - attribute.Converter, - attribute.Validators - ); - } + internal string GetFormattedIdentifier() => + !Property.IsEnumerable ? $"<{Name}>" : $"<{Name}...>"; } diff --git a/CliFx/Schema/PropertyDescriptor.cs b/CliFx/Schema/PropertyDescriptor.cs new file mode 100644 index 0000000..0fed85c --- /dev/null +++ b/CliFx/Schema/PropertyDescriptor.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace CliFx.Schema; + +/// +/// Describes a CLR property. +/// +public class PropertyDescriptor( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + Type type, + Func getValue, + Action setValue +) +{ + /// + /// Property's CLR type. + /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + public Type Type { get; } = type; + + /// + /// Gets the current value of the property on the specified instance. + /// + public object? GetValue(object instance) => getValue(instance); + + /// + /// Sets the value of the property on the specified instance. + /// + public void SetValue(object instance, object? value) => setValue(instance, value); +}