mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
asd
This commit is contained in:
@@ -14,10 +14,10 @@
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageReference Include="xunit" Version="2.8.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit" Version="2.8.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- Make sure to target the lowest possible version of the compiler for wider support -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="PolyShim" Version="1.10.0" PrivateAssets="all" />
|
||||
<PackageReference Include="PolyShim" Version="1.12.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -17,10 +17,10 @@
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="PolyShim" Version="1.10.0" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit" Version="2.8.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="PolyShim" Version="1.12.0" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit" Version="2.8.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
public class ApplicationConfiguration(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
ApplicationSchema schema,
|
||||
bool isDebugModeAllowed,
|
||||
bool isPreviewModeAllowed
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Command types defined in the application.
|
||||
/// Application schema.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Type> CommandTypes { get; } = commandTypes;
|
||||
public ApplicationSchema Schema { get; } = schema;
|
||||
|
||||
/// <summary>
|
||||
/// Whether debug mode is allowed in the application.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public partial class CliApplicationBuilder
|
||||
{
|
||||
private readonly HashSet<Type> _commandTypes = [];
|
||||
private readonly HashSet<CommandSchema> _commandSchemas = [];
|
||||
|
||||
private bool _isDebugModeAllowed = true;
|
||||
private bool _isPreviewModeAllowed = true;
|
||||
@@ -30,71 +29,12 @@ public partial class CliApplicationBuilder
|
||||
/// <summary>
|
||||
/// Adds a command to the application.
|
||||
/// </summary>
|
||||
public CliApplicationBuilder AddCommand(Type commandType)
|
||||
public CliApplicationBuilder AddCommand(CommandSchema commandSchema)
|
||||
{
|
||||
_commandTypes.Add(commandType);
|
||||
_commandSchemas.Add(commandSchema);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a command to the application.
|
||||
/// </summary>
|
||||
public CliApplicationBuilder AddCommand<TCommand>()
|
||||
where TCommand : ICommand => AddCommand(typeof(TCommand));
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple commands to the application.
|
||||
/// </summary>
|
||||
public CliApplicationBuilder AddCommands(IEnumerable<Type> commandTypes)
|
||||
{
|
||||
foreach (var commandType in commandTypes)
|
||||
AddCommand(commandType);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds commands from the specified assembly to the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method looks for public non-abstract classes that implement <see cref="ICommand" />
|
||||
/// and are annotated by <see cref="CommandAttribute" />.
|
||||
/// </remarks>
|
||||
public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
|
||||
{
|
||||
foreach (
|
||||
var commandType in commandAssembly.ExportedTypes.Where(CommandSchema.IsCommandType)
|
||||
)
|
||||
AddCommand(commandType);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds commands from the specified assemblies to the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method looks for public non-abstract classes that implement <see cref="ICommand" />
|
||||
/// and are annotated by <see cref="CommandAttribute" />.
|
||||
/// </remarks>
|
||||
public CliApplicationBuilder AddCommandsFrom(IEnumerable<Assembly> commandAssemblies)
|
||||
{
|
||||
foreach (var commandAssembly in commandAssemblies)
|
||||
AddCommandsFrom(commandAssembly);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds commands from the calling assembly to the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method looks for public non-abstract classes that implement <see cref="ICommand" />
|
||||
/// and are annotated by <see cref="CommandAttribute" />.
|
||||
/// </remarks>
|
||||
public CliApplicationBuilder AddCommandsFromThisAssembly() =>
|
||||
AddCommandsFrom(Assembly.GetCallingAssembly());
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application.
|
||||
/// </summary>
|
||||
@@ -189,15 +129,6 @@ public partial class CliApplicationBuilder
|
||||
// Null returns are handled by DelegateTypeActivator
|
||||
UseTypeActivator(serviceProvider.GetService!);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public CliApplicationBuilder UseTypeActivator(
|
||||
Func<IReadOnlyList<Type>, IServiceProvider> getServiceProvider
|
||||
) => UseTypeActivator(getServiceProvider(_commandTypes.ToArray()));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a configured instance of <see cref="CliApplication" />.
|
||||
/// </summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="PolyShim" Version="1.10.0" PrivateAssets="all" />
|
||||
<PackageReference Include="PolyShim" Version="1.12.0" PrivateAssets="all" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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<IBindingConverter>(
|
||||
memberSchema.ConverterType
|
||||
);
|
||||
var converter = typeActivator.CreateInstance<IBindingConverter>(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<string> 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<string> rawValues)
|
||||
private object? ConvertMember(IInputSchema inputSchema, IReadOnlyList<string> 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<BindingValidationError>();
|
||||
|
||||
foreach (var validatorType in memberSchema.ValidatorTypes)
|
||||
foreach (var validatorType in inputSchema.Validators)
|
||||
{
|
||||
var validator = typeActivator.CreateInstance<IBindingValidator>(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<string> 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(
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
namespace CliFx.Extensibility;
|
||||
|
||||
// Used internally to simplify the usage from reflection
|
||||
internal interface IBindingConverter
|
||||
/// <summary>
|
||||
/// Defines a custom conversion for binding command-line arguments to command inputs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To implement your own converter, inherit from <see cref="BindingConverter{T}" /> instead.
|
||||
/// </remarks>
|
||||
public interface IBindingConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the value from a raw command-line argument.
|
||||
/// </summary>
|
||||
object? Convert(string? rawValue);
|
||||
}
|
||||
|
||||
@@ -12,7 +20,7 @@ internal interface IBindingConverter
|
||||
public abstract class BindingConverter<T> : IBindingConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses value from a raw command-line argument.
|
||||
/// Parses the value from a raw command-line argument.
|
||||
/// </summary>
|
||||
public abstract T Convert(string? rawValue);
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
namespace CliFx.Extensibility;
|
||||
|
||||
// Used internally to simplify the usage from reflection
|
||||
internal interface IBindingValidator
|
||||
/// <summary>
|
||||
/// Defines a custom validation rules for values bound from command-line arguments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To implement your own validator, inherit from <see cref="BindingValidator{T}" /> instead.
|
||||
/// </remarks>
|
||||
public interface IBindingValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the value bound to a parameter or an option.
|
||||
/// Returns null if validation is successful, or an error in case of failure.
|
||||
/// </summary>
|
||||
BindingValidationError? Validate(object? value);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, [], []);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ internal class HelpContext(
|
||||
ApplicationMetadata applicationMetadata,
|
||||
ApplicationSchema applicationSchema,
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyDictionary<IMemberSchema, object?> commandDefaultValues
|
||||
IReadOnlyDictionary<IInputSchema, object?> commandDefaultValues
|
||||
)
|
||||
{
|
||||
public ApplicationMetadata ApplicationMetadata { get; } = applicationMetadata;
|
||||
@@ -16,6 +16,6 @@ internal class HelpContext(
|
||||
|
||||
public CommandSchema CommandSchema { get; } = commandSchema;
|
||||
|
||||
public IReadOnlyDictionary<IMemberSchema, object?> CommandDefaultValues { get; } =
|
||||
public IReadOnlyDictionary<IInputSchema, object?> CommandDefaultValues { get; } =
|
||||
commandDefaultValues;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public object CreateInstance(Type type)
|
||||
public object CreateInstance(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -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<Type, object> createInstance) : ITypeActivator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public object CreateInstance(Type type) =>
|
||||
public object CreateInstance(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type
|
||||
) =>
|
||||
createInstance(type)
|
||||
?? throw CliFxException.InternalError(
|
||||
$"""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx.Infrastructure;
|
||||
@@ -11,12 +12,17 @@ public interface ITypeActivator
|
||||
/// <summary>
|
||||
/// Creates an instance of the specified type.
|
||||
/// </summary>
|
||||
object CreateInstance(Type type);
|
||||
object CreateInstance(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type
|
||||
);
|
||||
}
|
||||
|
||||
internal static class TypeActivatorExtensions
|
||||
{
|
||||
public static T CreateInstance<T>(this ITypeActivator activator, Type type)
|
||||
public static T CreateInstance<T>(
|
||||
this ITypeActivator activator,
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type
|
||||
)
|
||||
{
|
||||
if (!typeof(T).IsAssignableFrom(type))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Schema;
|
||||
|
||||
namespace CliFx.Input;
|
||||
|
||||
@@ -9,10 +8,6 @@ internal class OptionInput(string identifier, IReadOnlyList<string> values)
|
||||
|
||||
public IReadOnlyList<string> Values { get; } = values;
|
||||
|
||||
public bool IsHelpOption => OptionSchema.HelpOption.MatchesIdentifier(Identifier);
|
||||
|
||||
public bool IsVersionOption => OptionSchema.VersionOption.MatchesIdentifier(Identifier);
|
||||
|
||||
public string GetFormattedIdentifier() =>
|
||||
Identifier switch
|
||||
{
|
||||
|
||||
@@ -5,33 +5,39 @@ using CliFx.Utils.Extensions;
|
||||
|
||||
namespace CliFx.Schema;
|
||||
|
||||
internal partial class ApplicationSchema(IReadOnlyList<CommandSchema> commands)
|
||||
/// <summary>
|
||||
/// Describes the structure of a command-line application.
|
||||
/// </summary>
|
||||
public class ApplicationSchema(IReadOnlyList<CommandSchema> commands)
|
||||
{
|
||||
/// <summary>
|
||||
/// Commands defined in the application.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CommandSchema> Commands { get; } = commands;
|
||||
|
||||
public IReadOnlyList<string> GetCommandNames() =>
|
||||
internal IReadOnlyList<string> 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<CommandSchema> GetDescendantCommands(
|
||||
IReadOnlyList<CommandSchema> potentialParentCommandSchemas,
|
||||
IReadOnlyList<CommandSchema> potentialDescendantCommands,
|
||||
string? parentCommandName
|
||||
)
|
||||
{
|
||||
var result = new List<CommandSchema>();
|
||||
|
||||
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<CommandSchema> 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<CommandSchema> GetDescendantCommands(string? parentCommandName) =>
|
||||
internal IReadOnlyList<CommandSchema> GetDescendantCommands(string? parentCommandName) =>
|
||||
GetDescendantCommands(Commands, parentCommandName);
|
||||
|
||||
public IReadOnlyList<CommandSchema> GetChildCommands(string? parentCommandName)
|
||||
internal IReadOnlyList<CommandSchema> GetChildCommands(string? parentCommandName)
|
||||
{
|
||||
var descendants = GetDescendantCommands(parentCommandName);
|
||||
|
||||
@@ -62,16 +68,8 @@ internal partial class ApplicationSchema(IReadOnlyList<CommandSchema> 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<Type> commandTypes) =>
|
||||
new(commandTypes.Select(CommandSchema.Resolve).ToArray());
|
||||
}
|
||||
|
||||
@@ -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<object?> 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<object?>();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
/// <summary>
|
||||
/// Describes an individual command.
|
||||
/// </summary>
|
||||
public class CommandSchema(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type,
|
||||
string? name,
|
||||
string? description,
|
||||
IReadOnlyList<ParameterSchema> parameters,
|
||||
IReadOnlyList<OptionSchema> options
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Command's CLR type.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
|
||||
public Type Type { get; } = type;
|
||||
|
||||
/// <summary>
|
||||
/// Command name.
|
||||
/// </summary>
|
||||
public string? Name { get; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Command description.
|
||||
/// </summary>
|
||||
public string? Description { get; } = description;
|
||||
|
||||
/// <summary>
|
||||
/// Command parameters.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ParameterSchema> Parameters { get; } = parameters;
|
||||
|
||||
/// <summary>
|
||||
/// Command options.
|
||||
/// </summary>
|
||||
public IReadOnlyList<OptionSchema> Options { get; } = options;
|
||||
|
||||
public bool IsDefault => string.IsNullOrWhiteSpace(Name);
|
||||
/// <summary>
|
||||
/// Whether this command is the application's default command.
|
||||
/// </summary>
|
||||
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<IMemberSchema, object?> GetValues(ICommand instance)
|
||||
internal IReadOnlyDictionary<IInputSchema, object?> GetValues(ICommand instance)
|
||||
{
|
||||
var result = new Dictionary<IMemberSchema, object?>();
|
||||
var result = new Dictionary<IInputSchema, object?>();
|
||||
|
||||
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<CommandAttribute>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
36
CliFx/Schema/IInputSchema.cs
Normal file
36
CliFx/Schema/IInputSchema.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Extensibility;
|
||||
|
||||
namespace CliFx.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Describes an input of a command, which can be either a parameter or an option.
|
||||
/// </summary>
|
||||
public interface IInputSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about the property that this input is bound to.
|
||||
/// </summary>
|
||||
PropertyDescriptor Property { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this input is a scalar (single value) or a sequence (multiple values).
|
||||
/// </summary>
|
||||
bool IsScalar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Valid values for this input, if applicable.
|
||||
/// If the input does not have a predefined set of valid values, this property is <c>null</c>.
|
||||
/// </summary>
|
||||
IReadOnlyList<object?>? ValidValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional binding converter for this input.
|
||||
/// </summary>
|
||||
IBindingConverter? Converter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional binding validator(s) for this input.
|
||||
/// </summary>
|
||||
IReadOnlyList<IBindingValidator> Validators { get; }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Schema;
|
||||
|
||||
internal interface IMemberSchema
|
||||
{
|
||||
IPropertyDescriptor Property { get; }
|
||||
|
||||
Type? ConverterType { get; }
|
||||
|
||||
IReadOnlyList<Type> 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))
|
||||
};
|
||||
}
|
||||
@@ -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<object?> GetValidValues();
|
||||
}
|
||||
|
||||
internal static class PropertyDescriptorExtensions
|
||||
{
|
||||
public static bool IsScalar(this IPropertyDescriptor propertyDescriptor) =>
|
||||
propertyDescriptor.Type == typeof(string)
|
||||
|| propertyDescriptor.Type.TryGetEnumerableUnderlyingType() is 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<object?> GetValidValues() => Array.Empty<object?>();
|
||||
}
|
||||
|
||||
internal partial class NullPropertyDescriptor
|
||||
{
|
||||
public static NullPropertyDescriptor Instance { get; } = new();
|
||||
}
|
||||
@@ -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,
|
||||
/// <summary>
|
||||
/// Describes a command's option.
|
||||
/// </summary>
|
||||
public class OptionSchema(
|
||||
PropertyDescriptor property,
|
||||
string? name,
|
||||
char? shortName,
|
||||
string? environmentVariable,
|
||||
bool isRequired,
|
||||
string? description,
|
||||
Type? converterType,
|
||||
IReadOnlyList<Type> validatorTypes
|
||||
) : IMemberSchema
|
||||
IBindingConverter? converter,
|
||||
IReadOnlyList<IBindingValidator> validators
|
||||
) : IInputSchema
|
||||
{
|
||||
public IPropertyDescriptor Property { get; } = property;
|
||||
/// <inheritdoc />
|
||||
public PropertyDescriptor Property { get; } = property;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsScalar { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<object?>? ValidValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Option name.
|
||||
/// </summary>
|
||||
public string? Name { get; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Option short name.
|
||||
/// </summary>
|
||||
public char? ShortName { get; } = shortName;
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable that can be used as a fallback for this option.
|
||||
/// </summary>
|
||||
public string? EnvironmentVariable { get; } = environmentVariable;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the option is required.
|
||||
/// </summary>
|
||||
public bool IsRequired { get; } = isRequired;
|
||||
|
||||
/// <summary>
|
||||
/// Option description.
|
||||
/// </summary>
|
||||
public string? Description { get; } = description;
|
||||
|
||||
public Type? ConverterType { get; } = converterType;
|
||||
/// <inheritdoc />
|
||||
public IBindingConverter? Converter { get; } = converter;
|
||||
|
||||
public IReadOnlyList<Type> ValidatorTypes { get; } = validatorTypes;
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IBindingValidator> 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<CommandOptionAttribute>();
|
||||
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<Type>()
|
||||
);
|
||||
|
||||
public static OptionSchema VersionOption { get; } =
|
||||
new(
|
||||
NullPropertyDescriptor.Instance,
|
||||
"version",
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"Shows version information.",
|
||||
null,
|
||||
Array.Empty<Type>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
/// <summary>
|
||||
/// Describes a command's parameter.
|
||||
/// </summary>
|
||||
public class ParameterSchema(
|
||||
PropertyDescriptor property,
|
||||
int order,
|
||||
string name,
|
||||
bool isRequired,
|
||||
string? description,
|
||||
Type? converterType,
|
||||
IReadOnlyList<Type> validatorTypes
|
||||
) : IMemberSchema
|
||||
IBindingConverter? converter,
|
||||
IReadOnlyList<IBindingValidator> validators
|
||||
) : IInputSchema
|
||||
{
|
||||
public IPropertyDescriptor Property { get; } = property;
|
||||
/// <inheritdoc />
|
||||
public PropertyDescriptor Property { get; } = property;
|
||||
|
||||
/// <summary>
|
||||
/// Order, in which the parameter is bound from the command-line arguments.
|
||||
/// </summary>
|
||||
public int Order { get; } = order;
|
||||
|
||||
/// <summary>
|
||||
/// Parameter name.
|
||||
/// </summary>
|
||||
public string Name { get; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the parameter is required.
|
||||
/// </summary>
|
||||
public bool IsRequired { get; } = isRequired;
|
||||
|
||||
/// <summary>
|
||||
/// Parameter description.
|
||||
/// </summary>
|
||||
public string? Description { get; } = description;
|
||||
|
||||
public Type? ConverterType { get; } = converterType;
|
||||
/// <inheritdoc />
|
||||
public IBindingConverter? Converter { get; } = converter;
|
||||
|
||||
public IReadOnlyList<Type> ValidatorTypes { get; } = validatorTypes;
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IBindingValidator> Validators { get; } = validators;
|
||||
|
||||
public string GetFormattedIdentifier() => Property.IsScalar() ? $"<{Name}>" : $"<{Name}...>";
|
||||
}
|
||||
|
||||
internal partial class ParameterSchema
|
||||
{
|
||||
public static ParameterSchema? TryResolve(PropertyInfo property)
|
||||
{
|
||||
var attribute = property.GetCustomAttribute<CommandParameterAttribute>();
|
||||
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}...>";
|
||||
}
|
||||
|
||||
31
CliFx/Schema/PropertyDescriptor.cs
Normal file
31
CliFx/Schema/PropertyDescriptor.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace CliFx.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a CLR property.
|
||||
/// </summary>
|
||||
public class PropertyDescriptor(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
|
||||
Type type,
|
||||
Func<object, object?> getValue,
|
||||
Action<object, object?> setValue
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Property's CLR type.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
|
||||
public Type Type { get; } = type;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of the property on the specified instance.
|
||||
/// </summary>
|
||||
public object? GetValue(object instance) => getValue(instance);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the property on the specified instance.
|
||||
/// </summary>
|
||||
public void SetValue(object instance, object? value) => setValue(instance, value);
|
||||
}
|
||||
Reference in New Issue
Block a user