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