From f7645afbdbac97bbe1714819150991d4d262c367 Mon Sep 17 00:00:00 2001
From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Date: Sun, 11 Aug 2024 01:44:40 +0300
Subject: [PATCH] asd
---
CliFx.Tests.Dummy/Program.cs | 3 +-
CliFx/Attributes/CommandParameterAttribute.cs | 6 +-
CliFx/Extensibility/BindingConverter.cs | 9 +-
CliFx/Extensibility/BoolBindingConverter.cs | 9 +-
.../ConvertibleBindingConverter.cs | 7 +-
.../DateTimeOffsetBindingConverter.cs | 7 +-
.../Extensibility/DelegateBindingConverter.cs | 14 ++-
CliFx/Extensibility/EnumBindingConverter.cs | 8 +-
CliFx/Extensibility/IBindingConverter.cs | 8 +-
CliFx/Extensibility/IBindingValidator.cs | 2 +-
CliFx/Extensibility/NoopBindingConverter.cs | 8 +-
.../Extensibility/NullableBindingConverter.cs | 9 +-
.../Extensibility/TimeSpanBindingConverter.cs | 6 +-
CliFx/FallbackDefaultCommand.cs | 5 +-
CliFx/IBindableCommand.cs | 2 +-
CliFx/Schema/CommandSchema.cs | 28 ++++--
CliFx/Schema/InputSchema.cs | 95 +++++++++++++++++--
CliFx/Schema/OptionSchema.cs | 38 +++++++-
CliFx/Schema/ParameterSchema.cs | 27 +++++-
CliFx/Schema/PropertyBinding.cs | 25 ++++-
CliFx/Utils/Extensions/TypeExtensions.cs | 2 +-
21 files changed, 254 insertions(+), 64 deletions(-)
diff --git a/CliFx.Tests.Dummy/Program.cs b/CliFx.Tests.Dummy/Program.cs
index eb6265e..7c44252 100644
--- a/CliFx.Tests.Dummy/Program.cs
+++ b/CliFx.Tests.Dummy/Program.cs
@@ -1,7 +1,6 @@
using System;
using System.IO;
using System.Reflection;
-using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace CliFx.Tests.Dummy;
@@ -13,7 +12,7 @@ public static class Program
public static string FilePath { get; } =
Path.ChangeExtension(
Assembly.GetExecutingAssembly().Location,
- RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "exe" : null
+ OperatingSystem.IsWindows() ? "exe" : null
);
public static async Task Main()
diff --git a/CliFx/Attributes/CommandParameterAttribute.cs b/CliFx/Attributes/CommandParameterAttribute.cs
index c377105..db5cc61 100644
--- a/CliFx/Attributes/CommandParameterAttribute.cs
+++ b/CliFx/Attributes/CommandParameterAttribute.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using CliFx.Extensibility;
namespace CliFx.Attributes;
@@ -15,8 +16,9 @@ public class CommandParameterAttribute(int order) : Attribute
///
///
/// All parameters in a command must have unique order.
- /// Parameter whose type is a non-scalar (e.g. array), must always be the last in order.
- /// Only one non-scalar parameter is allowed in a command.
+ /// Parameter whose type is a sequence (e.g. Array, ; except ),
+ /// must always be the last parameter based on order.
+ /// Only one sequential parameter is allowed in a command.
///
public int Order { get; } = order;
diff --git a/CliFx/Extensibility/BindingConverter.cs b/CliFx/Extensibility/BindingConverter.cs
index 49e5ec5..469934b 100644
--- a/CliFx/Extensibility/BindingConverter.cs
+++ b/CliFx/Extensibility/BindingConverter.cs
@@ -1,4 +1,6 @@
-namespace CliFx.Extensibility;
+using System;
+
+namespace CliFx.Extensibility;
///
/// Base type for custom converters.
@@ -8,7 +10,8 @@ public abstract class BindingConverter : IBindingConverter
///
/// Parses the value from a raw command-line argument.
///
- public abstract T? Convert(string? rawValue);
+ public abstract T? Convert(string? rawValue, IFormatProvider? formatProvider);
- object? IBindingConverter.Convert(string? rawValue) => Convert(rawValue);
+ object? IBindingConverter.Convert(string? rawValue, IFormatProvider? formatProvider) =>
+ Convert(rawValue, formatProvider);
}
diff --git a/CliFx/Extensibility/BoolBindingConverter.cs b/CliFx/Extensibility/BoolBindingConverter.cs
index 25347a5..b004638 100644
--- a/CliFx/Extensibility/BoolBindingConverter.cs
+++ b/CliFx/Extensibility/BoolBindingConverter.cs
@@ -1,4 +1,6 @@
-namespace CliFx.Extensibility;
+using System;
+
+namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties of type .
@@ -6,5 +8,6 @@
public class BoolBindingConverter : BindingConverter
{
///
- public override bool Convert(string? rawValue) => string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue);
-}
\ No newline at end of file
+ public override bool Convert(string? rawValue, IFormatProvider? formatProvider) =>
+ string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue);
+}
diff --git a/CliFx/Extensibility/ConvertibleBindingConverter.cs b/CliFx/Extensibility/ConvertibleBindingConverter.cs
index c989ab0..ae51ffe 100644
--- a/CliFx/Extensibility/ConvertibleBindingConverter.cs
+++ b/CliFx/Extensibility/ConvertibleBindingConverter.cs
@@ -5,9 +5,10 @@ namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties that implement .
///
-public class ConvertibleBindingConverter(IFormatProvider formatProvider) : BindingConverter where T: IConvertible
+public class ConvertibleBindingConverter : BindingConverter
+ where T : IConvertible
{
///
- public override T? Convert(string? rawValue) =>
+ public override T? Convert(string? rawValue, IFormatProvider? formatProvider) =>
(T?)System.Convert.ChangeType(rawValue, typeof(T), formatProvider);
-}
\ No newline at end of file
+}
diff --git a/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs b/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs
index 1a00845..35dd428 100644
--- a/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs
+++ b/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs
@@ -5,8 +5,9 @@ namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties of type .
///
-public class DateTimeOffsetBindingConverter(IFormatProvider formatProvider) : BindingConverter
+public class DateTimeOffsetBindingConverter : BindingConverter
{
///
- public override DateTimeOffset Convert(string? rawValue) => DateTimeOffset.Parse(rawValue!, formatProvider);
-}
\ No newline at end of file
+ public override DateTimeOffset Convert(string? rawValue, IFormatProvider? formatProvider) =>
+ DateTimeOffset.Parse(rawValue!, formatProvider);
+}
diff --git a/CliFx/Extensibility/DelegateBindingConverter.cs b/CliFx/Extensibility/DelegateBindingConverter.cs
index a96ecf7..5292ea9 100644
--- a/CliFx/Extensibility/DelegateBindingConverter.cs
+++ b/CliFx/Extensibility/DelegateBindingConverter.cs
@@ -5,8 +5,16 @@ namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties using a custom delegate.
///
-public class DelegateBindingConverter(Func convert) : BindingConverter
+public class DelegateBindingConverter(Func convert)
+ : BindingConverter
{
+ ///
+ /// Initializes an instance of
+ ///
+ public DelegateBindingConverter(Func convert)
+ : this((rawValue, _) => convert(rawValue)) { }
+
///
- public override T? Convert(string? rawValue) => convert(rawValue);
-}
\ No newline at end of file
+ public override T Convert(string? rawValue, IFormatProvider? formatProvider) =>
+ convert(rawValue, formatProvider);
+}
diff --git a/CliFx/Extensibility/EnumBindingConverter.cs b/CliFx/Extensibility/EnumBindingConverter.cs
index 92ec1dd..f87b002 100644
--- a/CliFx/Extensibility/EnumBindingConverter.cs
+++ b/CliFx/Extensibility/EnumBindingConverter.cs
@@ -5,8 +5,10 @@ namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties of type .
///
-public class EnumBindingConverter : BindingConverter where T : struct, Enum
+public class EnumBindingConverter : BindingConverter
+ where T : struct, Enum
{
///
- public override T Convert(string? rawValue) => (T)Enum.Parse(typeof(T), rawValue!, true);
-}
\ No newline at end of file
+ public override T Convert(string? rawValue, IFormatProvider? formatProvider) =>
+ (T)Enum.Parse(typeof(T), rawValue!, true);
+}
diff --git a/CliFx/Extensibility/IBindingConverter.cs b/CliFx/Extensibility/IBindingConverter.cs
index b50b9a4..a87c8a3 100644
--- a/CliFx/Extensibility/IBindingConverter.cs
+++ b/CliFx/Extensibility/IBindingConverter.cs
@@ -1,4 +1,6 @@
-namespace CliFx.Extensibility;
+using System;
+
+namespace CliFx.Extensibility;
///
/// Defines a custom conversion for binding command-line arguments to command inputs.
@@ -11,5 +13,5 @@ public interface IBindingConverter
///
/// Parses the value from a raw command-line argument.
///
- object? Convert(string? rawValue);
-}
\ No newline at end of file
+ object? Convert(string? rawValue, IFormatProvider? formatProvider);
+}
diff --git a/CliFx/Extensibility/IBindingValidator.cs b/CliFx/Extensibility/IBindingValidator.cs
index 7e13070..97cf561 100644
--- a/CliFx/Extensibility/IBindingValidator.cs
+++ b/CliFx/Extensibility/IBindingValidator.cs
@@ -13,4 +13,4 @@ public interface IBindingValidator
/// Returns null if validation is successful, or an error in case of failure.
///
BindingValidationError? Validate(object? value);
-}
\ No newline at end of file
+}
diff --git a/CliFx/Extensibility/NoopBindingConverter.cs b/CliFx/Extensibility/NoopBindingConverter.cs
index 1b8c106..c977065 100644
--- a/CliFx/Extensibility/NoopBindingConverter.cs
+++ b/CliFx/Extensibility/NoopBindingConverter.cs
@@ -1,4 +1,6 @@
-namespace CliFx.Extensibility;
+using System;
+
+namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties without any conversion.
@@ -6,5 +8,5 @@
public class NoopBindingConverter : IBindingConverter
{
///
- public object? Convert(string? rawValue) => rawValue;
-}
\ No newline at end of file
+ public object? Convert(string? rawValue, IFormatProvider? formatProvider) => rawValue;
+}
diff --git a/CliFx/Extensibility/NullableBindingConverter.cs b/CliFx/Extensibility/NullableBindingConverter.cs
index 22b8f4b..be59b98 100644
--- a/CliFx/Extensibility/NullableBindingConverter.cs
+++ b/CliFx/Extensibility/NullableBindingConverter.cs
@@ -5,11 +5,12 @@ namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties of type .
///
-public class NullableBindingConverter(BindingConverter innerConverter) : BindingConverter where T : struct
+public class NullableBindingConverter(BindingConverter innerConverter) : BindingConverter
+ where T : struct
{
///
- public override T? Convert(string? rawValue) =>
+ public override T? Convert(string? rawValue, IFormatProvider? formatProvider) =>
!string.IsNullOrWhiteSpace(rawValue)
- ? innerConverter.Convert(rawValue)
+ ? innerConverter.Convert(rawValue, formatProvider)
: null;
-}
\ No newline at end of file
+}
diff --git a/CliFx/Extensibility/TimeSpanBindingConverter.cs b/CliFx/Extensibility/TimeSpanBindingConverter.cs
index c785a05..37804d5 100644
--- a/CliFx/Extensibility/TimeSpanBindingConverter.cs
+++ b/CliFx/Extensibility/TimeSpanBindingConverter.cs
@@ -5,9 +5,9 @@ namespace CliFx.Extensibility;
///
/// Converter for binding inputs to properties of type .
///
-public class TimeSpanBindingConverter(IFormatProvider formatProvider) : BindingConverter
+public class TimeSpanBindingConverter : BindingConverter
{
///
- public override TimeSpan Convert(string? rawValue) =>
+ public override TimeSpan Convert(string? rawValue, IFormatProvider? formatProvider) =>
TimeSpan.Parse(rawValue!, formatProvider);
-}
\ No newline at end of file
+}
diff --git a/CliFx/FallbackDefaultCommand.cs b/CliFx/FallbackDefaultCommand.cs
index 1109115..0ce3928 100644
--- a/CliFx/FallbackDefaultCommand.cs
+++ b/CliFx/FallbackDefaultCommand.cs
@@ -10,7 +10,10 @@ 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 partial class FallbackDefaultCommand : IBindableCommand, ICommandWithHelpOption, ICommandWithVersionOption
+internal partial class FallbackDefaultCommand
+ : IBindableCommand,
+ ICommandWithHelpOption,
+ ICommandWithVersionOption
{
[CommandHelpOption]
public bool IsHelpRequested { get; init; }
diff --git a/CliFx/IBindableCommand.cs b/CliFx/IBindableCommand.cs
index 830f61a..c9c449a 100644
--- a/CliFx/IBindableCommand.cs
+++ b/CliFx/IBindableCommand.cs
@@ -17,4 +17,4 @@ public interface IBindableCommand : ICommand
/// Binds the command input to the current instance.
///
void Bind(CommandInput input);
-}
\ No newline at end of file
+}
diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs
index b9fc517..df551f8 100644
--- a/CliFx/Schema/CommandSchema.cs
+++ b/CliFx/Schema/CommandSchema.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
namespace CliFx.Schema;
@@ -11,8 +12,7 @@ public class CommandSchema(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type,
string? name,
string? description,
- IReadOnlyList parameters,
- IReadOnlyList options
+ IReadOnlyList inputs
)
{
///
@@ -36,15 +36,21 @@ public class CommandSchema(
///
public string? Description { get; } = description;
+ ///
+ /// Inputs (parameters and options) of the command.
+ ///
+ public IReadOnlyList Inputs { get; } = inputs;
+
///
/// Parameter inputs of the command.
///
- public IReadOnlyList Parameters { get; } = parameters;
+ public IReadOnlyList Parameters { get; } =
+ inputs.OfType().ToArray();
///
/// Option inputs of the command.
///
- public IReadOnlyList Options { get; } = options;
+ public IReadOnlyList Options { get; } = inputs.OfType().ToArray();
internal bool MatchesName(string? name) =>
!string.IsNullOrWhiteSpace(Name)
@@ -57,16 +63,26 @@ public class CommandSchema(
foreach (var parameterSchema in Parameters)
{
- var value = parameterSchema.Property.GetValue(instance);
+ var value = parameterSchema.Property.Get(instance);
result[parameterSchema] = value;
}
foreach (var optionSchema in Options)
{
- var value = optionSchema.Property.GetValue(instance);
+ var value = optionSchema.Property.Get(instance);
result[optionSchema] = value;
}
return result;
}
}
+
+// Generic version of the type is used to simplify initialization from the source-generated code
+// and to enforce static references to all the types used in the binding.
+// The non-generic version is used internally by the framework when operating in a dynamic context.
+///
+public class CommandSchema<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TCommand
+>(string? name, string? description, IReadOnlyList inputs)
+ : CommandSchema(typeof(TCommand), name, description, inputs)
+ where TCommand : ICommand;
diff --git a/CliFx/Schema/InputSchema.cs b/CliFx/Schema/InputSchema.cs
index 4f4e260..95e1afc 100644
--- a/CliFx/Schema/InputSchema.cs
+++ b/CliFx/Schema/InputSchema.cs
@@ -1,5 +1,11 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using CliFx.Exceptions;
using CliFx.Extensibility;
+using CliFx.Utils.Extensions;
namespace CliFx.Schema;
@@ -8,28 +14,99 @@ namespace CliFx.Schema;
///
public abstract class InputSchema(
PropertyBinding property,
- bool isSequence,
- IBindingConverter? converter,
+ IBindingConverter converter,
IReadOnlyList validators
)
{
+ internal bool IsSequence { get; } =
+ property.Type != typeof(string)
+ && property.Type.TryGetEnumerableUnderlyingType() is not null;
+
///
/// CLR property to which this input is bound.
///
public PropertyBinding Property { get; } = property;
- ///
- /// Whether the input can accept more than one value.
- ///
- public bool IsSequence { get; } = isSequence;
-
///
/// Optional binding converter for this input.
///
- public IBindingConverter? Converter { get; } = converter;
+ public IBindingConverter Converter { get; } = converter;
///
/// Optional binding validator(s) for this input.
///
public IReadOnlyList Validators { get; } = validators;
+
+ internal void Validate(object? value)
+ {
+ var errors = new List();
+
+ foreach (var validator in validators)
+ {
+ var error = validator.Validate(value);
+
+ if (error is not null)
+ errors.Add(error);
+ }
+
+ if (errors.Any())
+ {
+ throw CliFxException.UserError(
+ $"""
+ {memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} has been provided with an invalid value.
+ Error(s):
+ {errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)}
+ """
+ );
+ }
+ }
+
+ internal void Set(ICommand command, IReadOnlyList rawInputs)
+ {
+ var formatProvider = CultureInfo.InvariantCulture;
+
+ // Multiple values expected, single or multiple values provided
+ if (IsSequence)
+ {
+ var value = rawInputs.Select(v => Converter.Convert(v, formatProvider)).ToArray();
+ Validate(value);
+
+ Property.Set(command, value);
+ }
+ // Single value expected, single value provided
+ else if (rawInputs.Count <= 1)
+ {
+ var value = Converter.Convert(rawInputs.SingleOrDefault(), formatProvider);
+ Validate(value);
+
+ Property.Set(command, value);
+ }
+ // Single value expected, multiple values provided
+ else
+ {
+ throw CliFxException.UserError(
+ $"""
+ {memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} expects a single argument, but provided with multiple:
+ {rawInputs.Select(v => '<' + v + '>').JoinToString(" ")}
+ """
+ );
+ }
+ }
}
+
+// Generic version of the type is used to simplify initialization from the source-generated code
+// and to enforce static references to all the types used in the binding.
+// The non-generic version is used internally by the framework when operating in a dynamic context.
+///
+public abstract class InputSchema<
+ TCommand,
+ [DynamicallyAccessedMembers(
+ DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
+ )]
+ TProperty
+>(
+ PropertyBinding property,
+ BindingConverter converter,
+ IReadOnlyList> validators
+) : InputSchema(property, converter, validators)
+ where TCommand : ICommand;
diff --git a/CliFx/Schema/OptionSchema.cs b/CliFx/Schema/OptionSchema.cs
index 493760e..03509a6 100644
--- a/CliFx/Schema/OptionSchema.cs
+++ b/CliFx/Schema/OptionSchema.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Text;
using CliFx.Extensibility;
@@ -10,15 +11,14 @@ namespace CliFx.Schema;
///
public class OptionSchema(
PropertyBinding property,
- bool isSequence,
string? name,
char? shortName,
string? environmentVariable,
bool isRequired,
string? description,
- IBindingConverter? converter,
+ IBindingConverter converter,
IReadOnlyList validators
-) : InputSchema(property, isSequence, converter, validators)
+) : InputSchema(property, converter, validators)
{
///
/// Option name.
@@ -84,3 +84,35 @@ public class OptionSchema(
return buffer.ToString();
}
}
+
+// Generic version of the type is used to simplify initialization from the source-generated code
+// and to enforce static references to all the types used in the binding.
+// The non-generic version is used internally by the framework when operating in a dynamic context.
+///
+public class OptionSchema<
+ TCommand,
+ [DynamicallyAccessedMembers(
+ DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
+ )]
+ TProperty
+>(
+ PropertyBinding property,
+ string? name,
+ char? shortName,
+ string? environmentVariable,
+ bool isRequired,
+ string? description,
+ BindingConverter converter,
+ IReadOnlyList> validators
+)
+ : OptionSchema(
+ property,
+ name,
+ shortName,
+ environmentVariable,
+ isRequired,
+ description,
+ converter,
+ validators
+ )
+ where TCommand : ICommand;
diff --git a/CliFx/Schema/ParameterSchema.cs b/CliFx/Schema/ParameterSchema.cs
index b52f40d..e7969fc 100644
--- a/CliFx/Schema/ParameterSchema.cs
+++ b/CliFx/Schema/ParameterSchema.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using CliFx.Extensibility;
namespace CliFx.Schema;
@@ -8,14 +9,13 @@ namespace CliFx.Schema;
///
public class ParameterSchema(
PropertyBinding property,
- bool isSequence,
int order,
string name,
bool isRequired,
string? description,
- IBindingConverter? converter,
+ IBindingConverter converter,
IReadOnlyList validators
-) : InputSchema(property, isSequence, converter, validators)
+) : InputSchema(property, converter, validators)
{
///
/// Order, in which the parameter is bound from the command-line arguments.
@@ -39,3 +39,24 @@ public class ParameterSchema(
internal string GetFormattedIdentifier() => IsSequence ? $"<{Name}>" : $"<{Name}...>";
}
+
+// Generic version of the type is used to simplify initialization from the source-generated code
+// and to enforce static references to all the types used in the binding.
+// The non-generic version is used internally by the framework when operating in a dynamic context.
+///
+public class ParameterSchema<
+ TCommand,
+ [DynamicallyAccessedMembers(
+ DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
+ )]
+ TProperty
+>(
+ PropertyBinding property,
+ int order,
+ string name,
+ bool isRequired,
+ string? description,
+ BindingConverter converter,
+ IReadOnlyList> validators
+) : ParameterSchema(property, order, name, isRequired, description, converter, validators)
+ where TCommand : ICommand;
diff --git a/CliFx/Schema/PropertyBinding.cs b/CliFx/Schema/PropertyBinding.cs
index fa3e651..26a8004 100644
--- a/CliFx/Schema/PropertyBinding.cs
+++ b/CliFx/Schema/PropertyBinding.cs
@@ -13,8 +13,8 @@ public class PropertyBinding(
DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
)]
Type type,
- Func