Refactor & improve argument conversion feature

This commit is contained in:
Tyrrrz
2020-10-23 20:52:26 +03:00
parent c06f2810b9
commit 8df1d607c1
21 changed files with 490 additions and 243 deletions

View File

@@ -140,6 +140,30 @@ public class MyCommand : ICommand
[CommandParameter(2)]
public IReadOnlyList<string> ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with valid converter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyConverter : IArgumentValueConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
@@ -157,7 +181,7 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Param { get; set; }
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -176,7 +200,7 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption(""foo"", 'f')]
public string Param { get; set; }
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -195,10 +219,10 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string ParamA { get; set; }
public string OptionA { get; set; }
[CommandOption(""bar"")]
public string ParamB { get; set; }
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -217,10 +241,10 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption('f')]
public string ParamA { get; set; }
public string OptionA { get; set; }
[CommandOption('x')]
public string ParamB { get; set; }
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -239,11 +263,35 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption('a', EnvironmentVariableName = ""env_var_a"")]
public string ParamA { get; set; }
public string OptionA { get; set; }
[CommandOption('b', EnvironmentVariableName = ""env_var_b"")]
public string ParamB { get; set; }
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with valid converter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyConverter : IArgumentValueConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Converter = typeof(MyConverter))]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
@@ -366,6 +414,30 @@ public class MyCommand : ICommand
[CommandParameter(2)]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with invalid converter",
DiagnosticDescriptors.CliFx0025,
// language=cs
@"
public class MyConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
@@ -383,7 +455,7 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption("""")]
public string Param { get; set; }
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -402,7 +474,7 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption(""a"")]
public string Param { get; set; }
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -421,10 +493,10 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string ParamA { get; set; }
public string OptionA { get; set; }
[CommandOption(""foo"")]
public string ParamB { get; set; }
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -443,10 +515,10 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption('f')]
public string ParamA { get; set; }
public string OptionA { get; set; }
[CommandOption('f')]
public string ParamB { get; set; }
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
@@ -465,11 +537,35 @@ public class MyCommand : ICommand
public class MyCommand : ICommand
{
[CommandOption('a', EnvironmentVariableName = ""env_var"")]
public string ParamA { get; set; }
public string OptionA { get; set; }
[CommandOption('b', EnvironmentVariableName = ""env_var"")]
public string ParamB { get; set; }
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with invalid converter",
DiagnosticDescriptors.CliFx0046,
// language=cs
@"
public class MyConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Converter = typeof(MyConverter))]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)

View File

@@ -17,16 +17,19 @@ namespace CliFx.Analyzers
DiagnosticDescriptors.CliFx0022,
DiagnosticDescriptors.CliFx0023,
DiagnosticDescriptors.CliFx0024,
DiagnosticDescriptors.CliFx0025,
DiagnosticDescriptors.CliFx0041,
DiagnosticDescriptors.CliFx0042,
DiagnosticDescriptors.CliFx0043,
DiagnosticDescriptors.CliFx0044,
DiagnosticDescriptors.CliFx0045
DiagnosticDescriptors.CliFx0045,
DiagnosticDescriptors.CliFx0046
);
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
KnownSymbols.IsSystemString(typeSymbol) ||
!typeSymbol.AllInterfaces.Select(i => i.ConstructedFrom).Any(KnownSymbols.IsSystemCollectionsGenericIEnumerable);
!typeSymbol.AllInterfaces.Select(i => i.ConstructedFrom)
.Any(KnownSymbols.IsSystemCollectionsGenericIEnumerable);
private static void CheckCommandParameterProperties(
SymbolAnalysisContext context,
@@ -50,11 +53,18 @@ namespace CliFx.Analyzers
.Select(a => a.Value.Value)
.FirstOrDefault() as string;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.FirstOrDefault() as ITypeSymbol;
return new
{
Property = p,
Order = order,
Name = name
Name = name,
Converter = converter
};
})
.ToArray();
@@ -69,8 +79,9 @@ namespace CliFx.Analyzers
foreach (var parameter in duplicateOrderParameters)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0021, parameter.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0021, parameter.Property.Locations.First()
));
}
// Duplicate name
@@ -83,8 +94,9 @@ namespace CliFx.Analyzers
foreach (var parameter in duplicateNameParameters)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0022, parameter.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0022, parameter.Property.Locations.First()
));
}
// Multiple non-scalar
@@ -96,8 +108,9 @@ namespace CliFx.Analyzers
{
foreach (var parameter in nonScalarParameters)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0023, parameter.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0023, parameter.Property.Locations.First()
));
}
}
@@ -109,8 +122,23 @@ namespace CliFx.Analyzers
if (nonLastNonScalarParameter != null)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0024, nonLastNonScalarParameter.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0024, nonLastNonScalarParameter.Property.Locations.First()
));
}
// Invalid converter
var invalidConverterParameters = parameters
.Where(p =>
p.Converter != null &&
!p.Converter.AllInterfaces.Any(KnownSymbols.IsArgumentValueConverterInterface))
.ToArray();
foreach (var parameter in invalidConverterParameters)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0025, parameter.Property.Locations.First()
));
}
}
@@ -143,12 +171,19 @@ namespace CliFx.Analyzers
.Select(a => a.Value.Value)
.FirstOrDefault() as string;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.FirstOrDefault() as ITypeSymbol;
return new
{
Property = p,
Name = name,
ShortName = shortName,
EnvironmentVariableName = envVarName
EnvironmentVariableName = envVarName,
Converter = converter
};
})
.ToArray();
@@ -160,8 +195,9 @@ namespace CliFx.Analyzers
foreach (var option in noNameOptions)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0041, option.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0041, option.Property.Locations.First()
));
}
// Too short name
@@ -171,8 +207,9 @@ namespace CliFx.Analyzers
foreach (var option in invalidNameLengthOptions)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0042, option.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0042, option.Property.Locations.First()
));
}
// Duplicate name
@@ -185,8 +222,9 @@ namespace CliFx.Analyzers
foreach (var option in duplicateNameOptions)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0043, option.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0043, option.Property.Locations.First()
));
}
// Duplicate name
@@ -199,8 +237,9 @@ namespace CliFx.Analyzers
foreach (var option in duplicateShortNameOptions)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0044, option.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0044, option.Property.Locations.First()
));
}
// Duplicate environment variable name
@@ -213,8 +252,23 @@ namespace CliFx.Analyzers
foreach (var option in duplicateEnvironmentVariableNameOptions)
{
context.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptors.CliFx0045, option.Property.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0045, option.Property.Locations.First()
));
}
// Invalid converter
var invalidConverterOptions = options
.Where(o =>
o.Converter != null &&
!o.Converter.AllInterfaces.Any(KnownSymbols.IsArgumentValueConverterInterface))
.ToArray();
foreach (var option in invalidConverterOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0046, option.Property.Locations.First()
));
}
}
@@ -252,10 +306,12 @@ namespace CliFx.Analyzers
var isAlmostValidCommandType = implementsCommandInterface ^ hasCommandAttribute;
if (isAlmostValidCommandType && !implementsCommandInterface)
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0001, namedTypeSymbol.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0001,
namedTypeSymbol.Locations.First()));
if (isAlmostValidCommandType && !hasCommandAttribute)
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0002, namedTypeSymbol.Locations.First()));
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0002,
namedTypeSymbol.Locations.First()));
return;
}

View File

@@ -8,72 +8,98 @@ namespace CliFx.Analyzers
new DiagnosticDescriptor(nameof(CliFx0001),
"Type must implement the 'CliFx.ICommand' interface in order to be a valid command",
"Type must implement the 'CliFx.ICommand' interface in order to be a valid command",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0002 =
new DiagnosticDescriptor(nameof(CliFx0002),
"Type must be annotated with the 'CliFx.Attributes.CommandAttribute' in order to be a valid command",
"Type must be annotated with the 'CliFx.Attributes.CommandAttribute' in order to be a valid command",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0021 =
new DiagnosticDescriptor(nameof(CliFx0021),
"Parameter order must be unique within its command",
"Parameter order must be unique within its command",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0022 =
new DiagnosticDescriptor(nameof(CliFx0022),
"Parameter order must have unique name within its command",
"Parameter order must have unique name within its command",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0023 =
new DiagnosticDescriptor(nameof(CliFx0023),
"Only one non-scalar parameter per command is allowed",
"Only one non-scalar parameter per command is allowed",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0024 =
new DiagnosticDescriptor(nameof(CliFx0024),
"Non-scalar parameter must be last in order",
"Non-scalar parameter must be last in order",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0025 =
new DiagnosticDescriptor(nameof(CliFx0025),
"Parameter converter must implement 'CliFx.IArgumentValueConverter'",
"Parameter converter must implement 'CliFx.IArgumentValueConverter'",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0041 =
new DiagnosticDescriptor(nameof(CliFx0041),
"Option must have a name or short name specified",
"Option must have a name or short name specified",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0042 =
new DiagnosticDescriptor(nameof(CliFx0042),
"Option name must be at least 2 characters long",
"Option name must be at least 2 characters long",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0043 =
new DiagnosticDescriptor(nameof(CliFx0043),
"Option name must be unique within its command",
"Option name must be unique within its command",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0044 =
new DiagnosticDescriptor(nameof(CliFx0044),
"Option short name must be unique within its command",
"Option short name must be unique within its command",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0045 =
new DiagnosticDescriptor(nameof(CliFx0045),
"Option environment variable name must be unique within its command",
"Option environment variable name must be unique within its command",
"Usage", DiagnosticSeverity.Error, true);
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0046 =
new DiagnosticDescriptor(nameof(CliFx0046),
"Option converter must implement 'CliFx.IArgumentValueConverter'",
"Option converter must implement 'CliFx.IArgumentValueConverter'",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0100 =
new DiagnosticDescriptor(nameof(CliFx0100),
"Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation",
"Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation",
"Usage", DiagnosticSeverity.Warning, true);
"Usage", DiagnosticSeverity.Warning, true
);
}
}

View File

@@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers
{
public static class KnownSymbols
internal static class KnownSymbols
{
public static bool IsSystemString(ISymbol symbol) =>
symbol.DisplayNameMatches("string") ||
@@ -25,6 +25,9 @@ namespace CliFx.Analyzers
public static bool IsCommandInterface(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.ICommand");
public static bool IsArgumentValueConverterInterface(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.IArgumentValueConverter");
public static bool IsCommandAttribute(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.Attributes.CommandAttribute");

View File

@@ -232,6 +232,26 @@ namespace CliFx.Tests
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_parameter_custom_converter_must_implement_the_corresponding_interface()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<InvalidCustomConverterParameterCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_names_that_are_not_empty()
{
@@ -371,5 +391,25 @@ namespace CliFx.Tests
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_option_custom_converter_must_implement_the_corresponding_interface()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<InvalidCustomConverterOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using CliFx.Tests.Commands.Converters;
using CliFx.Tests.Internal;
using FluentAssertions;
using Xunit;
@@ -18,7 +17,7 @@ namespace CliFx.Tests
public ArgumentConversionSpecs(ITestOutputHelper output) => _output = output;
[Fact]
public async Task Property_of_type_object_is_bound_directly_from_the_argument_value()
public async Task Argument_value_can_be_bound_to_object()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -46,7 +45,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_object_array_is_bound_directly_from_the_argument_values()
public async Task Argument_values_can_be_bound_to_array_of_object()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -74,7 +73,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_string_is_bound_directly_from_the_argument_value()
public async Task Argument_value_can_be_bound_to_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -102,7 +101,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_string_array_is_bound_directly_from_the_argument_values()
public async Task Argument_values_can_be_bound_to_array_of_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -130,7 +129,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_string_IEnumerable_is_bound_directly_from_the_argument_values()
public async Task Argument_values_can_be_bound_to_IEnumerable_of_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -158,7 +157,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_string_IReadOnlyList_is_bound_directly_from_the_argument_values()
public async Task Argument_values_can_be_bound_to_IReadOnlyList_of_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -186,7 +185,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_string_List_is_bound_directly_from_the_argument_values()
public async Task Argument_values_can_be_bound_to_List_of_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -214,7 +213,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_string_HashSet_is_bound_directly_from_the_argument_values()
public async Task Argument_values_can_be_bound_to_HashSet_of_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -242,7 +241,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_true()
public async Task Argument_value_can_be_bound_to_boolean_as_true_if_the_value_is_true()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -270,7 +269,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_bool_is_bound_as_false_if_the_argument_value_is_false()
public async Task Argument_value_can_be_bound_to_boolean_as_false_if_the_value_is_false()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -298,7 +297,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_not_set()
public async Task Argument_value_can_be_bound_to_boolean_as_true_if_the_value_is_not_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -326,7 +325,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_char_is_bound_directly_from_the_argument_value_if_it_contains_only_one_character()
public async Task Argument_value_can_be_bound_to_char_if_the_value_contains_a_single_character()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -354,7 +353,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_sbyte_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_sbyte()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -382,7 +381,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_byte_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_byte()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -410,7 +409,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_short_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_short()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -438,7 +437,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_ushort_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_ushort()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -466,7 +465,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_int_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_int()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -494,7 +493,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_nullable_int_is_bound_by_parsing_the_argument_value_if_it_is_set()
public async Task Argument_value_can_be_bound_to_nullable_of_int_as_actual_value_if_it_is_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -522,7 +521,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_nullable_int_is_bound_as_null_if_the_argument_value_is_not_set()
public async Task Argument_value_can_be_bound_to_nullable_of_int_as_null_if_it_is_not_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -550,7 +549,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_int_array_is_bound_by_parsing_the_argument_values()
public async Task Argument_values_can_be_bound_to_array_of_int()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -578,7 +577,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_nullable_int_array_is_bound_by_parsing_the_argument_values()
public async Task Argument_values_can_be_bound_to_array_of_nullable_of_int()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -606,7 +605,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_uint_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_uint()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -634,7 +633,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_long_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_long()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -662,7 +661,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_ulong_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_ulong()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -690,7 +689,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_float_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_float()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -718,7 +717,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_double_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_double()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -746,7 +745,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_decimal_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_decimal()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -774,7 +773,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_DateTime_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_DateTime()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -802,7 +801,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_DateTimeOffset_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_DateTimeOffset()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -830,7 +829,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_TimeSpan_is_bound_by_parsing_the_argument_value()
public async Task Argument_value_can_be_bound_to_TimeSpan()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -858,7 +857,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_nullable_TimeSpan_is_bound_by_parsing_the_argument_value_if_it_is_set()
public async Task Argument_value_can_be_bound_to_nullable_of_TimeSpan_as_actual_value_if_it_is_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -886,7 +885,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_type_nullable_TimeSpan_is_bound_as_null_if_the_argument_value_is_not_set()
public async Task Argument_value_can_be_bound_to_nullable_of_TimeSpan_as_null_if_it_is_not_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -914,7 +913,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_name()
public async Task Argument_value_can_be_bound_to_an_enum_type_by_name()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -942,7 +941,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_id()
public async Task Argument_value_can_be_bound_to_an_enum_type_by_id()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -970,7 +969,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_name_if_it_is_set()
public async Task Argument_value_can_be_bound_to_nullable_of_enum_type_by_name_if_it_is_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -998,7 +997,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_id_if_it_is_set()
public async Task Argument_value_can_be_bound_to_nullable_of_enum_type_by_id_if_it_is_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1026,7 +1025,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_a_nullable_enum_type_is_bound_as_null_if_the_argument_value_is_not_set()
public async Task Argument_value_can_be_bound_to_nullable_of_enum_type_as_null_if_it_is_not_set()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1054,7 +1053,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_names()
public async Task Argument_values_can_be_bound_to_array_of_enum_type_by_names()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1082,7 +1081,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_ids()
public async Task Argument_values_can_be_bound_to_array_of_enum_type_by_ids()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1110,7 +1109,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_either_names_or_ids()
public async Task Argument_values_can_be_bound_to_array_of_enum_type_by_either_names_or_ids()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1138,7 +1137,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_a_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_value()
public async Task Argument_value_can_be_bound_to_a_custom_type_if_it_has_a_constructor_accepting_a_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1166,7 +1165,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_an_array_of_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_values()
public async Task Argument_values_can_be_bound_to_array_of_custom_type_if_it_has_a_constructor_accepting_a_string()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1198,7 +1197,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_is_bound_by_invoking_the_method()
public async Task Argument_value_can_be_bound_to_a_custom_type_if_it_has_a_static_Parse_method()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1226,7 +1225,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_and_format_provider_is_bound_by_invoking_the_method()
public async Task Argument_value_can_be_bound_to_a_custom_type_if_it_has_a_static_Parse_method_with_format_provider()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
@@ -1255,55 +1254,72 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_of_custom_type_must_be_string_initializable_in_order_to_be_bound()
public async Task Argument_value_can_be_bound_to_a_custom_type_if_a_converter_has_been_specified()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<UnsupportedArgumentTypesCommand>()
.AddCommand<SupportedArgumentTypesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--str-non-initializable", "foobar"
"cmd", "--convertible", "13"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
var commandInstance = stdOut.GetString().DeserializeJson<SupportedArgumentTypesCommand>();
_output.WriteLine(stdErr.GetString());
// Assert
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand
{
Convertible =
(SupportedArgumentTypesCommand.CustomConvertible)
new SupportedArgumentTypesCommand.CustomConvertibleConverter().ConvertFrom("13")
});
}
[Fact]
public async Task Property_of_custom_type_that_implements_IEnumerable_can_only_be_bound_if_that_type_has_a_constructor_accepting_an_array()
public async Task Argument_values_can_be_bound_to_array_of_custom_type_if_a_converter_has_been_specified()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<UnsupportedArgumentTypesCommand>()
.AddCommand<SupportedArgumentTypesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--str-enumerable-non-initializable", "foobar"
"cmd", "--convertible-array", "13", "42"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
var commandInstance = stdOut.GetString().DeserializeJson<SupportedArgumentTypesCommand>();
_output.WriteLine(stdErr.GetString());
// Assert
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand
{
ConvertibleArray = new[]
{
(SupportedArgumentTypesCommand.CustomConvertible)
new SupportedArgumentTypesCommand.CustomConvertibleConverter().ConvertFrom("13"),
(SupportedArgumentTypesCommand.CustomConvertible)
new SupportedArgumentTypesCommand.CustomConvertibleConverter().ConvertFrom("42")
}
});
}
[Fact]
public async Task Property_of_non_nullable_type_can_only_be_bound_if_the_argument_value_is_set()
public async Task Argument_value_can_only_be_bound_to_non_nullable_type_if_it_is_set()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
@@ -1327,7 +1343,7 @@ namespace CliFx.Tests
}
[Fact]
public async Task Property_must_have_a_type_that_implements_IEnumerable_in_order_to_be_bound_from_multiple_argument_values()
public async Task Argument_values_can_only_be_bound_to_a_type_that_implements_IEnumerable()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
@@ -1349,70 +1365,5 @@ namespace CliFx.Tests
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Property_of_custom_type_is_bound_when_the_valid_converter_type_is_specified()
{
// Arrange
const string foo = "foo";
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<CommandWithParameterOfCustomType>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--prop", foo
});
// Assert
exitCode.Should().Be(0);
var commandInstance = stdOut.GetString().DeserializeJson<CommandWithParameterOfCustomType>();
commandInstance.Should().BeEquivalentTo(new CommandWithParameterOfCustomType()
{
MyProperty = (CustomType) new CustomTypeConverter().ConvertFrom(foo)
});
}
[Fact]
public async Task Enumerable_of_the_custom_type_is_bound_when_the_valid_converter_type_is_specified()
{
// Arrange
string foo = "foo";
string bar = "bar";
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<CommandWithEnumerableOfParametersOfCustomType>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--prop", foo, bar
});
// Assert
exitCode.Should().Be(0);
var commandInstance = stdOut.GetString().DeserializeJson<CommandWithEnumerableOfParametersOfCustomType>();
commandInstance.Should().BeEquivalentTo(new CommandWithEnumerableOfParametersOfCustomType()
{
MyProperties = new List<CustomType>
{
(CustomType) new CustomTypeConverter().ConvertFrom(foo),
(CustomType) new CustomTypeConverter().ConvertFrom(bar)
}
});
}
}
}

View File

@@ -1,27 +0,0 @@
using CliFx.Attributes;
using System;
using System.Threading.Tasks;
using CliFx.Tests.Commands.Converters;
using System.Collections.Generic;
namespace CliFx.Tests.Commands
{
public class CustomType
{
public int SomeValue { get; set; }
}
[Command("cmd")]
public class CommandWithParameterOfCustomType : SelfSerializeCommandBase
{
[CommandOption("prop", Converter = typeof(CustomTypeConverter))]
public CustomType? MyProperty { get; set; }
}
[Command("cmd")]
public class CommandWithEnumerableOfParametersOfCustomType : SelfSerializeCommandBase
{
[CommandOption("prop", Converter = typeof(CustomTypeConverter))]
public List<CustomType>? MyProperties { get; set; }
}
}

View File

@@ -1,8 +0,0 @@
namespace CliFx.Tests.Commands.Converters
{
public class CustomTypeConverter : IArgumentValueConverter
{
public object ConvertFrom(string value) =>
new CustomType { SomeValue = value.Length };
}
}

View File

@@ -0,0 +1,16 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class InvalidCustomConverterOptionCommand : SelfSerializeCommandBase
{
[CommandOption('f', Converter = typeof(Converter))]
public string? Option { get; set; }
public class Converter
{
public object ConvertFrom(string value) => value;
}
}
}

View File

@@ -0,0 +1,16 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class InvalidCustomConverterParameterCommand : SelfSerializeCommandBase
{
[CommandParameter(0, Converter = typeof(Converter))]
public string? Param { get; set; }
public class Converter
{
public object ConvertFrom(string value) => value;
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using CliFx.Attributes;
using Newtonsoft.Json;
@@ -84,6 +84,9 @@ namespace CliFx.Tests.Commands
[CommandOption("str-parseable-format")]
public CustomStringParseableWithFormatProvider? StringParseableWithFormatProvider { get; set; }
[CommandOption("convertible", Converter = typeof(CustomConvertibleConverter))]
public CustomConvertible? Convertible { get; set; }
[CommandOption("obj-array")]
public object[]? ObjectArray { get; set; }
@@ -102,6 +105,9 @@ namespace CliFx.Tests.Commands
[CommandOption("str-constructible-array")]
public CustomStringConstructible[]? StringConstructibleArray { get; set; }
[CommandOption("convertible-array", Converter = typeof(CustomConvertibleConverter))]
public CustomConvertible[]? ConvertibleArray { get; set; }
[CommandOption("str-enumerable")]
public IEnumerable<string>? StringEnumerable { get; set; }
@@ -151,5 +157,18 @@ namespace CliFx.Tests.Commands
public static CustomStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new CustomStringParseableWithFormatProvider(value + " " + formatProvider);
}
public class CustomConvertible
{
public int Value { get; }
public CustomConvertible(int value) => Value = value;
}
public class CustomConvertibleConverter : IArgumentValueConverter
{
public object ConvertFrom(string value) =>
new CustomConvertible(int.Parse(value, CultureInfo.InvariantCulture));
}
}
}

View File

@@ -29,7 +29,8 @@ namespace CliFx.Tests
// Act
var exitCode = await application.RunAsync(
new[] {"[preview]", "named", "param", "-abc", "--option", "foo"},
new Dictionary<string, string>());
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);

View File

@@ -38,7 +38,8 @@ namespace CliFx.Attributes
public string? EnvironmentVariableName { get; set; }
/// <summary>
/// Type of a converter to use for the option value evaluating.
/// Type of converter to use when mapping the argument value.
/// Converter must implement <see cref="IArgumentValueConverter"/>.
/// </summary>
public Type? Converter { get; set; }

View File

@@ -27,7 +27,8 @@ namespace CliFx.Attributes
public string? Description { get; set; }
/// <summary>
/// Type of a converter to use for the parameter value evaluating.
/// Type of converter to use when mapping the argument value.
/// Converter must implement <see cref="IArgumentValueConverter"/>.
/// </summary>
public Type? Converter { get; set; }

View File

@@ -17,13 +17,13 @@ namespace CliFx.Domain
public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null;
protected Type? Converter { get; set; }
public Type? ConverterType { get; }
protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converter = null)
protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converterType)
{
Property = property;
Description = description;
Converter = converter;
ConverterType = converterType;
}
private Type? TryGetEnumerableArgumentUnderlyingType() =>
@@ -35,6 +35,10 @@ namespace CliFx.Domain
{
try
{
// Custom conversion
if (ConverterType != null)
return ConverterType.CreateInstance<IArgumentValueConverter>().ConvertFrom(value!);
// Primitive
var primitiveConverter = PrimitiveConverters.GetValueOrDefault(targetType);
if (primitiveConverter != null)
@@ -57,17 +61,14 @@ namespace CliFx.Domain
return stringConstructor.Invoke(new object[] {value!});
// String-parseable (with format provider)
var parseMethodWithFormatProvider = targetType.GetStaticParseMethod(true);
var parseMethodWithFormatProvider = targetType.TryGetStaticParseMethod(true);
if (parseMethodWithFormatProvider != null)
return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, FormatProvider});
// String-parseable (without format provider)
var parseMethod = targetType.GetStaticParseMethod();
var parseMethod = targetType.TryGetStaticParseMethod();
if (parseMethod != null)
return parseMethod.Invoke(null, new object[] {value!});
if (Converter != null)
return Converter.InstanceOf<IArgumentValueConverter>().ConvertFrom(value!);
}
catch (Exception ex)
{

View File

@@ -24,8 +24,8 @@ namespace CliFx.Domain
string? environmentVariableName,
bool isRequired,
string? description,
Type? converter = null)
: base(property, description, converter)
Type? converterType)
: base(property, description, converterType)
{
Name = name;
ShortName = shortName;
@@ -107,9 +107,9 @@ namespace CliFx.Domain
internal partial class CommandOptionSchema
{
public static CommandOptionSchema HelpOption { get; } =
new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text.");
new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text.", null);
public static CommandOptionSchema VersionOption { get; } =
new CommandOptionSchema(null, "version", null, null, false, "Shows version information.");
new CommandOptionSchema(null, "version", null, null, false, "Shows version information.", null);
}
}

View File

@@ -12,8 +12,13 @@ namespace CliFx.Domain
public string Name { get; }
public CommandParameterSchema(PropertyInfo? property, int order, string name, string? description, Type? converter = null)
: base(property, description, converter)
public CommandParameterSchema(
PropertyInfo? property,
int order,
string name,
string? description,
Type? converterType)
: base(property, description, converterType)
{
Order = order;
Name = name;

View File

@@ -114,6 +114,18 @@ namespace CliFx.Domain
nonLastNonScalarParameter
);
}
var invalidConverterParameters = command.Parameters
.Where(p => p.ConverterType != null && !p.ConverterType.Implements(typeof(IArgumentValueConverter)))
.ToArray();
if (invalidConverterParameters.Any())
{
throw CliFxException.ParametersWithInvalidConverters(
command,
invalidConverterParameters
);
}
}
private static void ValidateOptions(CommandSchema command)
@@ -184,6 +196,18 @@ namespace CliFx.Domain
duplicateEnvironmentVariableNameGroup.ToArray()
);
}
var invalidConverterOptions = command.Options
.Where(o => o.ConverterType != null && !o.ConverterType.Implements(typeof(IArgumentValueConverter)))
.ToArray();
if (invalidConverterOptions.Any())
{
throw CliFxException.OptionsWithInvalidConverters(
command,
invalidConverterOptions
);
}
}
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)

View File

@@ -172,6 +172,19 @@ If it's not feasible to fit into these constraints, consider using options inste
return new CliFxException(message.Trim());
}
internal static CliFxException ParametersWithInvalidConverters(
CommandSchema command,
IReadOnlyList<CommandParameterSchema> invalidParameters)
{
var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameter(s) with invalid converters:
{invalidParameters.JoinToString(Environment.NewLine)}
Specified converter must implement {typeof(IArgumentValueConverter).FullName}.";
return new CliFxException(message.Trim());
}
internal static CliFxException OptionsWithNoName(
CommandSchema command,
IReadOnlyList<CommandOptionSchema> invalidOptions)
@@ -243,6 +256,19 @@ Environment variable names are not case-sensitive.";
return new CliFxException(message.Trim());
}
internal static CliFxException OptionsWithInvalidConverters(
CommandSchema command,
IReadOnlyList<CommandOptionSchema> invalidOptions)
{
var message = $@"
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} option(s) with invalid converters:
{invalidOptions.JoinToString(Environment.NewLine)}
Specified converter must implement {typeof(IArgumentValueConverter).FullName}.";
return new CliFxException(message.Trim());
}
}
// End-user-facing exceptions

View File

@@ -1,7 +1,7 @@
namespace CliFx
{
/// <summary>
/// Used as an interface for implementing custom parameter/option converters.
/// Implements custom conversion logic that maps an argument value to a domain type.
/// </summary>
public interface IArgumentValueConverter
{

View File

@@ -8,6 +8,8 @@ namespace CliFx.Internal.Extensions
{
internal static class TypeExtensions
{
public static T CreateInstance<T>(this Type type) => (T) Activator.CreateInstance(type);
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
@@ -31,11 +33,14 @@ namespace CliFx.Internal.Extensions
.FirstOrDefault();
}
public static MethodInfo GetToStringMethod(this Type type) => type.GetMethod(nameof(ToString), Type.EmptyTypes);
public static MethodInfo GetToStringMethod(this Type type) =>
// ToString() with no params always exists
type.GetMethod(nameof(ToString), Type.EmptyTypes)!;
public static bool IsToStringOverriden(this Type type) => type.GetToStringMethod() != typeof(object).GetToStringMethod();
public static bool IsToStringOverriden(this Type type) =>
type.GetToStringMethod() != typeof(object).GetToStringMethod();
public static MethodInfo GetStaticParseMethod(this Type type, bool withFormatProvider = false)
public static MethodInfo? TryGetStaticParseMethod(this Type type, bool withFormatProvider = false)
{
var argumentTypes = withFormatProvider
? new[] {typeof(string), typeof(IFormatProvider)}
@@ -56,10 +61,5 @@ namespace CliFx.Internal.Extensions
return array;
}
public static T InstanceOf<T>(this Type type) =>
type.Implements(typeof(T))
? (T) Activator.CreateInstance(type)
: throw new ArgumentException();
}
}