This commit is contained in:
Tyrrrz
2024-09-03 02:03:50 +03:00
parent 0532d724a1
commit a62ce71424
29 changed files with 124 additions and 208 deletions

View File

@@ -22,7 +22,8 @@ public class LibraryProvider
var data = File.ReadAllText(StorageFilePath); var data = File.ReadAllText(StorageFilePath);
return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library) ?? Library.Empty; return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library)
?? Library.Empty;
} }
public Book? TryGetBook(string title) => public Book? TryGetBook(string title) =>

View File

@@ -4,9 +4,10 @@ namespace CliFx.Attributes;
/// <summary> /// <summary>
/// Annotates a type that defines a command. /// Annotates a type that defines a command.
/// If a command is named, then the user must provide its name through the command-line arguments in order to execute it. /// If the command is named, then the user must provide its name through the
/// If a command is not named, then it is treated as the application's default command and is executed when no other /// command-line arguments in order to execute it.
/// command is specified. /// If the command is not named, then it is treated as the application's
/// default command and is executed whenever the user does not provide a command name.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Only one default command is allowed per application. /// Only one default command is allowed per application.

View File

@@ -4,7 +4,7 @@
/// Binds a property to the help option of a command. /// Binds a property to the help option of a command.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This attribute is applied automatically by the framework and should not be used explicitly. /// This attribute is applied automatically by the framework and should not need to be used explicitly.
/// </remarks> /// </remarks>
public class CommandHelpOptionAttribute : CommandOptionAttribute public class CommandHelpOptionAttribute : CommandOptionAttribute
{ {

View File

@@ -1,5 +1,4 @@
using System; using System;
using CliFx.Extensibility;
namespace CliFx.Attributes; namespace CliFx.Attributes;
@@ -7,7 +6,8 @@ namespace CliFx.Attributes;
/// Binds a property to a command option — a command-line input that is identified by a name and/or a short name. /// Binds a property to a command option — a command-line input that is identified by a name and/or a short name.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// All options in a command must have unique names (comparison IS NOT case-sensitive) and short names (comparison IS case-sensitive). /// All options in a command must have unique names (comparison IS NOT case-sensitive)
/// and short names (comparison IS case-sensitive).
/// </remarks> /// </remarks>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class CommandOptionAttribute : CommandInputAttribute public class CommandOptionAttribute : CommandInputAttribute
@@ -51,7 +51,7 @@ public class CommandOptionAttribute : CommandInputAttribute
/// <summary> /// <summary>
/// Whether this option is required (default: <c>false</c>). /// Whether this option is required (default: <c>false</c>).
/// If an option is required, the user will get an error if they don't set it. /// If an option is required, the user will get an error when they don't set it.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// You can use the <c>required</c> keyword on the property (introduced in C# 11) to implicitly /// You can use the <c>required</c> keyword on the property (introduced in C# 11) to implicitly

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using CliFx.Extensibility;
namespace CliFx.Attributes; namespace CliFx.Attributes;
@@ -10,7 +9,7 @@ namespace CliFx.Attributes;
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// All parameters in a command must have unique order values. /// All parameters in a command must have unique order values.
/// If a parameter is bound to a property whose type is a sequence (e.g. Array, <see cref="List{T}" />; except <see cref="string" />), /// If a parameter is bound to a property whose type is a sequence (i.e. implements <see cref="IEnumerable{T}"/>; except <see cref="string" />),
/// then it must have the highest order in the command. /// then it must have the highest order in the command.
/// Only one sequential parameter is allowed per command. /// Only one sequential parameter is allowed per command.
/// </remarks> /// </remarks>
@@ -24,7 +23,7 @@ public class CommandParameterAttribute(int order) : CommandInputAttribute
/// <summary> /// <summary>
/// Whether this parameter is required (default: <c>true</c>). /// Whether this parameter is required (default: <c>true</c>).
/// If a parameter is required, the user will get an error if they don't set it. /// If a parameter is required, the user will get an error when they don't set it.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Parameter marked as non-required must have the highest order in the command. /// Parameter marked as non-required must have the highest order in the command.

View File

@@ -4,7 +4,7 @@
/// Binds a property to the version option of a command. /// Binds a property to the version option of a command.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This attribute is applied automatically by the framework and should not be used explicitly. /// This attribute is applied automatically by the framework and should not need to be used explicitly.
/// </remarks> /// </remarks>
public class CommandVersionOptionAttribute : CommandOptionAttribute public class CommandVersionOptionAttribute : CommandOptionAttribute
{ {

View File

@@ -3,7 +3,7 @@
namespace CliFx.Exceptions; namespace CliFx.Exceptions;
/// <summary> /// <summary>
/// Exception thrown when there is an error during application execution. /// Exception thrown within <see cref="CliFx" />.
/// </summary> /// </summary>
public partial class CliFxException( public partial class CliFxException(
string message, string message,

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties of type <see cref="bool" />. /// Converter for activating command inputs bound to properties of type <see cref="bool" />.
/// </summary> /// </summary>
public class BoolBindingConverter : BindingConverter<bool> public class BoolBindingConverter : BindingConverter<bool>
{ {

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties whose types implement <see cref="IConvertible" />. /// Converter for activating command inputs bound to properties whose types implement <see cref="IConvertible" />.
/// </summary> /// </summary>
public class ConvertibleBindingConverter<T> : BindingConverter<T> public class ConvertibleBindingConverter<T> : BindingConverter<T>
where T : IConvertible where T : IConvertible

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties of type <see cref="DateTimeOffset" />. /// Converter for activating command inputs bound to properties of type <see cref="DateTimeOffset" />.
/// </summary> /// </summary>
public class DateTimeOffsetBindingConverter : BindingConverter<DateTimeOffset> public class DateTimeOffsetBindingConverter : BindingConverter<DateTimeOffset>
{ {

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties using a custom delegate. /// Converter for activating command inputs bound to properties using a custom delegate.
/// </summary> /// </summary>
public class DelegateBindingConverter<T>(Func<string?, IFormatProvider?, T> convert) public class DelegateBindingConverter<T>(Func<string?, IFormatProvider?, T> convert)
: BindingConverter<T> : BindingConverter<T>

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties of type <see cref="Enum" />. /// Converter for activating command inputs bound to properties of type <see cref="Enum" />.
/// </summary> /// </summary>
public class EnumBindingConverter<T> : BindingConverter<T> public class EnumBindingConverter<T> : BindingConverter<T>
where T : struct, Enum where T : struct, Enum

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties without any conversion. /// Converter for activating command inputs bound to properties without performing any conversion.
/// </summary> /// </summary>
public class NoopBindingConverter : IBindingConverter public class NoopBindingConverter : IBindingConverter
{ {

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties of type <see cref="Nullable{T}" />. /// Converter for activating command inputs bound to properties of type <see cref="Nullable{T}" />.
/// </summary> /// </summary>
public class NullableBindingConverter<T>(BindingConverter<T> innerConverter) : BindingConverter<T?> public class NullableBindingConverter<T>(BindingConverter<T> innerConverter) : BindingConverter<T?>
where T : struct where T : struct

View File

@@ -3,7 +3,7 @@
namespace CliFx.Extensibility; namespace CliFx.Extensibility;
/// <summary> /// <summary>
/// Converter for binding command inputs to properties of type <see cref="TimeSpan" />. /// Converter for activating command inputs bound to properties of type <see cref="TimeSpan" />.
/// </summary> /// </summary>
public class TimeSpanBindingConverter : BindingConverter<TimeSpan> public class TimeSpanBindingConverter : BindingConverter<TimeSpan>
{ {

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -307,39 +306,6 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con
if (defaultValue is null) if (defaultValue is null)
return; return;
// Non-Scalar
if (defaultValue is not string && defaultValue is IEnumerable defaultValues)
{
var elementType =
schema.Property.Type.TryGetEnumerableUnderlyingType() ?? typeof(object);
if (elementType.IsToStringOverriden())
{
Write(ConsoleColor.White, "Default: ");
var isFirst = true;
foreach (var element in defaultValues)
{
if (isFirst)
{
isFirst = false;
}
else
{
Write(", ");
}
Write('"');
Write(element.ToString(CultureInfo.InvariantCulture));
Write('"');
}
Write('.');
}
}
else
{
if (schema.Property.Type.IsToStringOverriden()) if (schema.Property.Type.IsToStringOverriden())
{ {
Write(ConsoleColor.White, "Default: "); Write(ConsoleColor.White, "Default: ");
@@ -350,7 +316,6 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con
Write('.'); Write('.');
} }
} }
}
private void WriteCommandChildren() private void WriteCommandChildren()
{ {

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Implements a <see cref="TextReader" /> for reading characters or binary data from a console stream. /// Implements a <see cref="StreamReader" /> for reading characters or binary data from a console stream.
/// </summary> /// </summary>
// Both the underlying stream AND the stream reader must be synchronized! // Both the underlying stream AND the stream reader must be synchronized!
// https://github.com/Tyrrrz/CliFx/issues/123 // https://github.com/Tyrrrz/CliFx/issues/123

View File

@@ -8,7 +8,7 @@ using CliFx.Utils;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Implements a <see cref="TextWriter" /> for writing characters or binary data to a console stream. /// Implements a <see cref="StreamWriter" /> for writing characters or binary data to a console stream.
/// </summary> /// </summary>
// Both the underlying stream AND the stream writer must be synchronized! // Both the underlying stream AND the stream writer must be synchronized!
// https://github.com/Tyrrrz/CliFx/issues/123 // https://github.com/Tyrrrz/CliFx/issues/123

View File

@@ -5,7 +5,7 @@ using CliFx.Exceptions;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Implementation of <see cref="ITypeActivator" /> that instantiates an object by using its parameterless constructor. /// Implementation of <see cref="ITypeActivator" /> that instantiates a type by using its parameterless constructor.
/// </summary> /// </summary>
public class DefaultTypeActivator : ITypeActivator public class DefaultTypeActivator : ITypeActivator
{ {

View File

@@ -5,7 +5,7 @@ using CliFx.Exceptions;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Implementation of <see cref="ITypeActivator" /> that instantiates an object by using a predefined delegate. /// Implementation of <see cref="ITypeActivator" /> that instantiates a type by using a predefined delegate.
/// </summary> /// </summary>
public class DelegateTypeActivator(Func<Type, object> createInstance) : ITypeActivator public class DelegateTypeActivator(Func<Type, object> createInstance) : ITypeActivator
{ {

View File

@@ -5,7 +5,7 @@ using CliFx.Exceptions;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Abstraction for a service that can instantiate objects at run-time. /// Abstraction for a service that can instantiate types at run-time.
/// </summary> /// </summary>
public interface ITypeActivator public interface ITypeActivator
{ {

View File

@@ -14,23 +14,21 @@ namespace CliFx.Schema;
/// </summary> /// </summary>
public abstract class CommandInputSchema( public abstract class CommandInputSchema(
PropertyBinding property, PropertyBinding property,
bool isSequence,
string? description, string? description,
IBindingConverter converter, IBindingConverter converter,
IReadOnlyList<IBindingValidator> validators IReadOnlyList<IBindingValidator> validators
) )
{ {
internal abstract string Kind { get; }
internal abstract string FormattedIdentifier { get; }
/// <summary> /// <summary>
/// CLR property to which this input is bound. /// CLR property to which this input is bound.
/// </summary> /// </summary>
public PropertyBinding Property { get; } = property; public PropertyBinding Property { get; } = property;
internal bool IsSequence { get; } = /// <summary>
property.Type != typeof(string) /// Whether this input is a sequence (i.e. multiple values can be provided).
&& property.Type.TryGetEnumerableUnderlyingType() is not null; /// </summary>
public bool IsSequence { get; } = isSequence;
/// <summary> /// <summary>
/// Input description, used in the help text. /// Input description, used in the help text.
@@ -51,14 +49,14 @@ public abstract class CommandInputSchema(
{ {
var errors = Validators var errors = Validators
.Select(validator => validator.Validate(value)) .Select(validator => validator.Validate(value))
.OfType<BindingValidationError>() .WhereNotNull()
.ToArray(); .ToArray();
if (errors.Any()) if (errors.Any())
{ {
throw CliFxException.UserError( throw CliFxException.UserError(
$""" $"""
{Kind} {FormattedIdentifier} has been provided with an invalid value. {this.GetKind()} {this.GetFormattedIdentifier()} has been provided with an invalid value.
Error(s): Error(s):
{errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)} {errors.Select(e => "- " + e.Message).JoinToString(Environment.NewLine)}
""" """
@@ -72,7 +70,7 @@ public abstract class CommandInputSchema(
try try
{ {
// Multiple values expected, single or multiple values provided // Sequential input; zero or more values provided
if (IsSequence) if (IsSequence)
{ {
var values = rawValues.Select(v => Converter.Convert(v, formatProvider)).ToArray(); var values = rawValues.Select(v => Converter.Convert(v, formatProvider)).ToArray();
@@ -80,23 +78,22 @@ public abstract class CommandInputSchema(
// TODO: cast array to the proper type // TODO: cast array to the proper type
Validate(values); Validate(values);
Property.SetValue(instance, values);
Property.Set(instance, values);
} }
// Single value expected, single value provided // Non-sequential input; zero or one value provided
else if (rawValues.Count <= 1) else if (rawValues.Count <= 1)
{ {
var value = Converter.Convert(rawValues.SingleOrDefault(), formatProvider); var value = Converter.Convert(rawValues.SingleOrDefault(), formatProvider);
Validate(value);
Property.Set(instance, value); Validate(value);
Property.SetValue(instance, value);
} }
// Single value expected, multiple values provided // Non-sequential input; more than one value provided
else else
{ {
throw CliFxException.UserError( throw CliFxException.UserError(
$""" $"""
{Kind} {FormattedIdentifier} expects a single value, but provided with multiple: {this.GetKind()} {this.GetFormattedIdentifier()} expects a single value, but provided with multiple:
{rawValues.Select(v => '<' + v + '>').JoinToString(" ")} {rawValues.Select(v => '<' + v + '>').JoinToString(" ")}
""" """
); );
@@ -106,7 +103,7 @@ public abstract class CommandInputSchema(
{ {
throw CliFxException.UserError( throw CliFxException.UserError(
$""" $"""
{Kind} {FormattedIdentifier} cannot be set from the provided value(s): {this.GetKind()} {this.GetFormattedIdentifier()} cannot be set from the provided value(s):
{rawValues.Select(v => '<' + v + '>').JoinToString(" ")} {rawValues.Select(v => '<' + v + '>').JoinToString(" ")}
Error: {ex.Message} Error: {ex.Message}
""", """,
@@ -117,7 +114,7 @@ public abstract class CommandInputSchema(
/// <inheritdoc /> /// <inheritdoc />
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public override string ToString() => FormattedIdentifier; public override string ToString() => this.GetFormattedIdentifier();
} }
/// <inheritdoc cref="CommandInputSchema" /> /// <inheritdoc cref="CommandInputSchema" />
@@ -134,8 +131,41 @@ public abstract class CommandInputSchema<
TProperty TProperty
>( >(
PropertyBinding<TCommand, TProperty> property, PropertyBinding<TCommand, TProperty> property,
bool isSequence,
string? description, string? description,
BindingConverter<TProperty> converter, BindingConverter<TProperty> converter,
IReadOnlyList<BindingValidator<TProperty>> validators IReadOnlyList<BindingValidator<TProperty>> validators
) : CommandInputSchema(property, description, converter, validators) ) : CommandInputSchema(property, isSequence, description, converter, validators)
where TCommand : ICommand; where TCommand : ICommand;
// Define these as extension methods to avoid exposing them as protected members (i.e. essentially public API)
internal static class CommandInputSchemaExtensions
{
public static string GetKind(this CommandInputSchema schema) =>
schema switch
{
CommandParameterSchema => "Parameter",
CommandOptionSchema => "Option",
_ => throw new InvalidOperationException("Unknown input schema type.")
};
public static string GetFormattedIdentifier(this CommandInputSchema schema) =>
schema switch
{
CommandParameterSchema parameter
=> parameter.IsSequence ? $"<{parameter.Name}>" : $"<{parameter.Name}...>",
CommandOptionSchema option
=> option switch
{
{ Name: not null, ShortName: not null }
=> $"-{option.ShortName}|--{option.Name}",
{ Name: not null } => $"--{option.Name}",
{ ShortName: not null } => $"-{option.ShortName}",
_
=> throw new InvalidOperationException(
"Option must have a name or a short name."
)
},
_ => throw new ArgumentOutOfRangeException(nameof(schema))
};
}

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text;
using CliFx.Extensibility; using CliFx.Extensibility;
namespace CliFx.Schema; namespace CliFx.Schema;
@@ -11,6 +10,7 @@ namespace CliFx.Schema;
/// </summary> /// </summary>
public class CommandOptionSchema( public class CommandOptionSchema(
PropertyBinding property, PropertyBinding property,
bool isSequence,
string? name, string? name,
char? shortName, char? shortName,
string? environmentVariable, string? environmentVariable,
@@ -18,38 +18,8 @@ public class CommandOptionSchema(
string? description, string? description,
IBindingConverter converter, IBindingConverter converter,
IReadOnlyList<IBindingValidator> validators IReadOnlyList<IBindingValidator> validators
) : CommandInputSchema(property, description, converter, validators) ) : CommandInputSchema(property, isSequence, description, converter, validators)
{ {
internal override string Kind => "Option";
internal override string FormattedIdentifier
{
get
{
var buffer = new StringBuilder();
// Short name
if (ShortName is not null)
{
buffer.Append('-').Append(ShortName);
}
// Separator
if (!string.IsNullOrWhiteSpace(Name) && ShortName is not null)
{
buffer.Append('|');
}
// Name
if (!string.IsNullOrWhiteSpace(Name))
{
buffer.Append("--").Append(Name);
}
return buffer.ToString();
}
}
/// <summary> /// <summary>
/// Option name. /// Option name.
/// </summary> /// </summary>
@@ -99,6 +69,7 @@ public class CommandOptionSchema<
TProperty TProperty
>( >(
PropertyBinding<TCommand, TProperty> property, PropertyBinding<TCommand, TProperty> property,
bool isSequence,
string? name, string? name,
char? shortName, char? shortName,
string? environmentVariable, string? environmentVariable,
@@ -109,6 +80,7 @@ public class CommandOptionSchema<
) )
: CommandOptionSchema( : CommandOptionSchema(
property, property,
isSequence,
name, name,
shortName, shortName,
environmentVariable, environmentVariable,

View File

@@ -9,20 +9,17 @@ namespace CliFx.Schema;
/// </summary> /// </summary>
public class CommandParameterSchema( public class CommandParameterSchema(
PropertyBinding property, PropertyBinding property,
bool isSequence,
int order, int order,
string name, string name,
bool isRequired, bool isRequired,
string? description, string? description,
IBindingConverter converter, IBindingConverter converter,
IReadOnlyList<IBindingValidator> validators IReadOnlyList<IBindingValidator> validators
) : CommandInputSchema(property, description, converter, validators) ) : CommandInputSchema(property, isSequence, description, converter, validators)
{ {
internal override string Kind => "Parameter";
internal override string FormattedIdentifier => IsSequence ? $"<{Name}>" : $"<{Name}...>";
/// <summary> /// <summary>
/// Order, in which the parameter is bound from the command-line arguments. /// Order, in which the parameter is activated from the command-line arguments.
/// </summary> /// </summary>
public int Order { get; } = order; public int Order { get; } = order;
@@ -51,11 +48,22 @@ public class CommandParameterSchema<
TProperty TProperty
>( >(
PropertyBinding<TCommand, TProperty> property, PropertyBinding<TCommand, TProperty> property,
bool isSequence,
int order, int order,
string name, string name,
bool isRequired, bool isRequired,
string? description, string? description,
BindingConverter<TProperty> converter, BindingConverter<TProperty> converter,
IReadOnlyList<BindingValidator<TProperty>> validators IReadOnlyList<BindingValidator<TProperty>> validators
) : CommandParameterSchema(property, order, name, isRequired, description, converter, validators) )
: CommandParameterSchema(
property,
isSequence,
order,
name,
isRequired,
description,
converter,
validators
)
where TCommand : ICommand; where TCommand : ICommand;

View File

@@ -62,24 +62,8 @@ public class CommandSchema(
? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase) ? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase)
: string.IsNullOrWhiteSpace(name); : string.IsNullOrWhiteSpace(name);
internal IReadOnlyDictionary<CommandInputSchema, object?> GetValues(ICommand instance) internal IReadOnlyDictionary<CommandInputSchema, object?> GetValues(ICommand instance) =>
{ Inputs.ToDictionary(input => input, input => input.Property.GetValue(instance));
var result = new Dictionary<CommandInputSchema, object?>();
foreach (var parameter in Parameters)
{
var value = parameter.Property.Get(instance);
result[parameter] = value;
}
foreach (var option in Options)
{
var value = option.Property.Get(instance);
result[option] = value;
}
return result;
}
private void ActivateParameters(ICommand instance, CommandArguments arguments) private void ActivateParameters(ICommand instance, CommandArguments arguments)
{ {
@@ -91,21 +75,20 @@ public class CommandSchema(
foreach (var parameter in Parameters.OrderBy(p => p.Order)) foreach (var parameter in Parameters.OrderBy(p => p.Order))
{ {
// Break when there are no remaining inputs // Break when there are no remaining tokens
if (position >= arguments.Parameters.Count) if (position >= arguments.Parameters.Count)
break; break;
// Sequence: take all remaining inputs starting from the current position // Sequential: take all remaining tokens starting from the current position
if (parameter.IsSequence) if (parameter.IsSequence)
{ {
var parameterTokens = arguments.Parameters.Skip(position).ToArray(); var parameterTokens = arguments.Parameters.Skip(position).ToArray();
parameter.Activate(instance, parameterTokens.Select(p => p.Value).ToArray()); parameter.Activate(instance, parameterTokens.Select(p => p.Value).ToArray());
position += parameterTokens.Length; position += parameterTokens.Length;
remainingParameterTokens.RemoveRange(parameterTokens); remainingParameterTokens.RemoveRange(parameterTokens);
} }
// Non-sequence: take one input at the current position // Non-sequential: take one token at the current position
else else
{ {
var parameterToken = arguments.Parameters[position]; var parameterToken = arguments.Parameters[position];
@@ -132,9 +115,9 @@ public class CommandSchema(
{ {
throw CliFxException.UserError( throw CliFxException.UserError(
$""" $"""
Missing equired parameter(s): Missing required parameter(s):
{remainingRequiredParameters {remainingRequiredParameters
.Select(p => p.FormattedIdentifier) .Select(p => p.GetFormattedIdentifier())
.JoinToString(" ")} .JoinToString(" ")}
""" """
); );
@@ -153,7 +136,7 @@ public class CommandSchema(
foreach (var option in Options) foreach (var option in Options)
{ {
var optionToken = arguments var optionTokens = arguments
.Options.Where(o => option.MatchesIdentifier(o.Identifier)) .Options.Where(o => option.MatchesIdentifier(o.Identifier))
.ToArray(); .ToArray();
@@ -161,10 +144,10 @@ public class CommandSchema(
option.MatchesEnvironmentVariable(v.Key) option.MatchesEnvironmentVariable(v.Key)
); );
// Direct input // From arguments
if (optionToken.Any()) if (optionTokens.Any())
{ {
var rawValues = optionToken.SelectMany(o => o.Values).ToArray(); var rawValues = optionTokens.SelectMany(o => o.Values).ToArray();
option.Activate(instance, rawValues); option.Activate(instance, rawValues);
@@ -172,7 +155,7 @@ public class CommandSchema(
if (rawValues.Any()) if (rawValues.Any())
remainingRequiredOptions.Remove(option); remainingRequiredOptions.Remove(option);
} }
// Environment variable // From environment
else if (!string.IsNullOrEmpty(environmentVariable.Value)) else if (!string.IsNullOrEmpty(environmentVariable.Value))
{ {
var rawValues = !option.IsSequence var rawValues = !option.IsSequence
@@ -194,7 +177,7 @@ public class CommandSchema(
continue; continue;
} }
remainingOptionTokens.RemoveRange(optionToken); remainingOptionTokens.RemoveRange(optionTokens);
} }
if (remainingOptionTokens.Any()) if (remainingOptionTokens.Any())
@@ -213,7 +196,7 @@ public class CommandSchema(
$""" $"""
Missing required option(s): Missing required option(s):
{remainingRequiredOptions {remainingRequiredOptions
.Select(o => o.FormattedIdentifier) .Select(o => o.GetFormattedIdentifier())
.JoinToString(", ")} .JoinToString(", ")}
""" """
); );

View File

@@ -13,8 +13,8 @@ public class PropertyBinding(
DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
)] )]
Type type, Type type,
Func<object, object?> get, Func<object, object?> getValue,
Action<object, object?> set Action<object, object?> setValue
) )
{ {
/// <summary> /// <summary>
@@ -28,12 +28,12 @@ public class PropertyBinding(
/// <summary> /// <summary>
/// Gets the current value of the property on the specified instance. /// Gets the current value of the property on the specified instance.
/// </summary> /// </summary>
public object? Get(object instance) => get(instance); public object? GetValue(object instance) => getValue(instance);
/// <summary> /// <summary>
/// Sets the current value of the property on the specified instance. /// Sets the current value of the property on the specified instance.
/// </summary> /// </summary>
public void Set(object instance, object? value) => set(instance, value); public void SetValue(object instance, object? value) => setValue(instance, value);
internal IReadOnlyList<object?>? TryGetValidValues() internal IReadOnlyList<object?>? TryGetValidValues()
{ {
@@ -67,9 +67,9 @@ public class PropertyBinding<
DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
)] )]
TProperty TProperty
>(Func<TObject, TProperty?> get, Action<TObject, TProperty?> set) >(Func<TObject, TProperty?> getValue, Action<TObject, TProperty?> setValue)
: PropertyBinding( : PropertyBinding(
typeof(TProperty), typeof(TProperty),
o => get((TObject)o), o => getValue((TObject)o),
(o, v) => set((TObject)o, (TProperty?)v) (o, v) => setValue((TObject)o, (TProperty?)v)
); );

View File

@@ -1,20 +0,0 @@
using System;
using System.Linq;
using System.Reflection;
namespace CliFx.Utils.Extensions;
internal static class PropertyExtensions
{
public static bool IsRequired(this PropertyInfo propertyInfo) =>
// Match attribute by name to avoid depending on .NET 7.0+ and to allow polyfilling
propertyInfo
.GetCustomAttributes()
.Any(a =>
string.Equals(
a.GetType().FullName,
"System.Runtime.CompilerServices.RequiredMemberAttribute",
StringComparison.Ordinal
)
);
}

View File

@@ -1,33 +1,10 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace CliFx.Utils.Extensions; namespace CliFx.Utils.Extensions;
internal static class TypeExtensions internal static class TypeExtensions
{ {
public static Type? TryGetEnumerableUnderlyingType(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type
) =>
type.GetInterfaces()
.Select(i =>
{
if (i == typeof(IEnumerable))
return typeof(object);
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return i.GetGenericArguments().FirstOrDefault();
return null;
})
.WhereNotNull()
// Every IEnumerable<T> implements IEnumerable (which is essentially IEnumerable<object>),
// so we try to get a more specific underlying type. Still, if the type only implements
// IEnumerable<object> and nothing else, then we'll just return that.
.MaxBy(t => t != typeof(object));
public static bool IsToStringOverriden( public static bool IsToStringOverriden(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type
) )