mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Add positional arguments (#32)
This commit is contained in:
committed by
Alexey Golub
parent
ed87373dc3
commit
e48839b938
@@ -14,7 +14,7 @@ namespace CliFx.Demo.Commands
|
|||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
private readonly LibraryService _libraryService;
|
||||||
|
|
||||||
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
[CommandArgument(0, Name = "title", IsRequired = true, Description = "Book title.")]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
|
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace CliFx.Tests
|
|||||||
.UseDescription("test")
|
.UseDescription("test")
|
||||||
.UseConsole(new VirtualConsole(TextWriter.Null))
|
.UseConsole(new VirtualConsole(TextWriter.Null))
|
||||||
.UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type!)!)
|
.UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type!)!)
|
||||||
.UseCommandOptionInputConverter(new CommandOptionInputConverter())
|
.UseCommandOptionInputConverter(new CommandInputConverter())
|
||||||
.UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub())
|
.UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub())
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|||||||
132
CliFx.Tests/Services/CommandArgumentSchemasValidatorTests.cs
Normal file
132
CliFx.Tests/Services/CommandArgumentSchemasValidatorTests.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using CliFx.Models;
|
||||||
|
using CliFx.Services;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.Services
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CommandArgumentSchemasValidatorTests
|
||||||
|
{
|
||||||
|
private static CommandArgumentSchema GetValidArgumentSchema(string propertyName, string name, bool isRequired, int order, string? description = null)
|
||||||
|
{
|
||||||
|
return new CommandArgumentSchema(typeof(TestCommand).GetProperty(propertyName)!, name, isRequired, description, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> GetTestCases_ValidatorTest()
|
||||||
|
{
|
||||||
|
// Validation should succeed when no arguments are supplied
|
||||||
|
yield return new TestCaseData(new ValidatorTest(new List<CommandArgumentSchema>(), true));
|
||||||
|
|
||||||
|
// Multiple sequence arguments
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "B", false, 1)
|
||||||
|
}, false));
|
||||||
|
|
||||||
|
// Argument after sequence
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1)
|
||||||
|
}, false));
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 1)
|
||||||
|
}, true));
|
||||||
|
|
||||||
|
// Required arguments must appear before optional arguments
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", true, 1)
|
||||||
|
}, false));
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", true, 1),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2),
|
||||||
|
}, false));
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", true, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", true, 2),
|
||||||
|
}, false));
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", true, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2),
|
||||||
|
}, true));
|
||||||
|
|
||||||
|
// Argument order must be unique
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2)
|
||||||
|
}, true));
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 1)
|
||||||
|
}, false));
|
||||||
|
|
||||||
|
// No arguments with the same name
|
||||||
|
yield return new TestCaseData(new ValidatorTest(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
|
||||||
|
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 1)
|
||||||
|
}, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestCommand
|
||||||
|
{
|
||||||
|
public IEnumerable<int> EnumerableProperty { get; set; }
|
||||||
|
public string StringProperty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidatorTest
|
||||||
|
{
|
||||||
|
public ValidatorTest(IReadOnlyCollection<CommandArgumentSchema> schemas, bool succeedsValidation)
|
||||||
|
{
|
||||||
|
Schemas = schemas;
|
||||||
|
SucceedsValidation = succeedsValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<CommandArgumentSchema> Schemas { get; }
|
||||||
|
public bool SucceedsValidation { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(GetTestCases_ValidatorTest))]
|
||||||
|
public void Validation_Test(ValidatorTest testCase)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validator = new CommandArgumentSchemasValidator();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = validator.ValidateArgumentSchemas(testCase.Schemas);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Any().Should().Be(!testCase.SucceedsValidation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ namespace CliFx.Tests.Services
|
|||||||
public class CommandFactoryTests
|
public class CommandFactoryTests
|
||||||
{
|
{
|
||||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] {commandType}).Single();
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,144 +16,215 @@ namespace CliFx.Tests.Services
|
|||||||
public class CommandInitializerTests
|
public class CommandInitializerTests
|
||||||
{
|
{
|
||||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||||
new CommandSchemaResolver().GetCommandSchemas(new[] { commandType }).Single();
|
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] { commandType }).Single();
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
|
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
|
||||||
{
|
{
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new DivideCommand(),
|
new DivideCommand(),
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("div", new[]
|
GetCommandSchema(typeof(DivideCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("dividend", "13"),
|
new CommandInput(new[] { "div" }, new[]
|
||||||
new CommandOptionInput("divisor", "8")
|
{
|
||||||
}),
|
new CommandOptionInput("dividend", "13"),
|
||||||
|
new CommandOptionInput("divisor", "8")
|
||||||
|
})),
|
||||||
new DivideCommand { Dividend = 13, Divisor = 8 }
|
new DivideCommand { Dividend = 13, Divisor = 8 }
|
||||||
);
|
);
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new DivideCommand(),
|
new DivideCommand(),
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("div", new[]
|
GetCommandSchema(typeof(DivideCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("dividend", "13"),
|
new CommandInput(new[] { "div" }, new[]
|
||||||
new CommandOptionInput("d", "8")
|
{
|
||||||
}),
|
new CommandOptionInput("dividend", "13"),
|
||||||
|
new CommandOptionInput("d", "8")
|
||||||
|
})),
|
||||||
new DivideCommand { Dividend = 13, Divisor = 8 }
|
new DivideCommand { Dividend = 13, Divisor = 8 }
|
||||||
);
|
);
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new DivideCommand(),
|
new DivideCommand(),
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("div", new[]
|
GetCommandSchema(typeof(DivideCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("D", "13"),
|
new CommandInput(new[] { "div" }, new[]
|
||||||
new CommandOptionInput("d", "8")
|
{
|
||||||
}),
|
new CommandOptionInput("D", "13"),
|
||||||
|
new CommandOptionInput("d", "8")
|
||||||
|
})),
|
||||||
new DivideCommand { Dividend = 13, Divisor = 8 }
|
new DivideCommand { Dividend = 13, Divisor = 8 }
|
||||||
);
|
);
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new ConcatCommand(),
|
new ConcatCommand(),
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("concat", new[]
|
GetCommandSchema(typeof(ConcatCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("i", new[] {"foo", " ", "bar"})
|
new CommandInput(new[] { "concat" }, new[]
|
||||||
}),
|
{
|
||||||
|
new CommandOptionInput("i", new[] { "foo", " ", "bar" })
|
||||||
|
})),
|
||||||
new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
|
new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
|
||||||
);
|
);
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new ConcatCommand(),
|
new ConcatCommand(),
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("concat", new[]
|
GetCommandSchema(typeof(ConcatCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("i", new[] {"foo", "bar"}),
|
new CommandInput(new[] { "concat" }, new[]
|
||||||
new CommandOptionInput("s", " ")
|
{
|
||||||
}),
|
new CommandOptionInput("i", new[] { "foo", "bar" }),
|
||||||
|
new CommandOptionInput("s", " ")
|
||||||
|
})),
|
||||||
new ConcatCommand { Inputs = new[] { "foo", "bar" }, Separator = " " }
|
new ConcatCommand { Inputs = new[] { "foo", "bar" }, Separator = " " }
|
||||||
);
|
);
|
||||||
|
|
||||||
//Will read a value from environment variables because none is supplied via CommandInput
|
//Will read a value from environment variables because none is supplied via CommandInput
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new EnvironmentVariableCommand(),
|
new EnvironmentVariableCommand(),
|
||||||
GetCommandSchema(typeof(EnvironmentVariableCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
|
GetCommandSchema(typeof(EnvironmentVariableCommand)),
|
||||||
|
new string[0],
|
||||||
|
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
|
||||||
new EnvironmentVariableCommand { Option = "A" }
|
new EnvironmentVariableCommand { Option = "A" }
|
||||||
);
|
);
|
||||||
|
|
||||||
//Will read multiple values from environment variables because none is supplied via CommandInput
|
//Will read multiple values from environment variables because none is supplied via CommandInput
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new EnvironmentVariableWithMultipleValuesCommand(),
|
new EnvironmentVariableWithMultipleValuesCommand(),
|
||||||
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
|
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
|
||||||
|
new string[0],
|
||||||
|
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
|
||||||
new EnvironmentVariableWithMultipleValuesCommand { Option = new[] { "A", "B", "C" } }
|
new EnvironmentVariableWithMultipleValuesCommand { Option = new[] { "A", "B", "C" } }
|
||||||
);
|
);
|
||||||
|
|
||||||
//Will not read a value from environment variables because one is supplied via CommandInput
|
//Will not read a value from environment variables because one is supplied via CommandInput
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new EnvironmentVariableCommand(),
|
new EnvironmentVariableCommand(),
|
||||||
GetCommandSchema(typeof(EnvironmentVariableCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput(null, new[]
|
GetCommandSchema(typeof(EnvironmentVariableCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("opt", new[] { "X" })
|
new CommandInput(new string[0], new[]
|
||||||
},
|
{
|
||||||
EnvironmentVariablesProviderStub.EnvironmentVariables),
|
new CommandOptionInput("opt", new[] { "X" })
|
||||||
|
},
|
||||||
|
EnvironmentVariablesProviderStub.EnvironmentVariables)),
|
||||||
new EnvironmentVariableCommand { Option = "X" }
|
new EnvironmentVariableCommand { Option = "X" }
|
||||||
);
|
);
|
||||||
|
|
||||||
//Will not split environment variable values because underlying property is not a collection
|
//Will not split environment variable values because underlying property is not a collection
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new EnvironmentVariableWithoutCollectionPropertyCommand(),
|
new EnvironmentVariableWithoutCollectionPropertyCommand(),
|
||||||
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
|
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
|
||||||
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
|
new string[0],
|
||||||
);
|
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
|
||||||
|
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Positional arguments
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new ArgumentCommand(),
|
||||||
|
new CommandCandidate(
|
||||||
|
GetCommandSchema(typeof(ArgumentCommand)),
|
||||||
|
new [] { "abc", "123", "1", "2" },
|
||||||
|
new CommandInput(new [] { "arg", "cmd", "abc", "123", "1", "2" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>())),
|
||||||
|
new ArgumentCommand { FirstArgument = "abc", SecondArgument = 123, ThirdArguments = new List<int>{1, 2}, Option = "option value" }
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new ArgumentCommand(),
|
||||||
|
new CommandCandidate(
|
||||||
|
GetCommandSchema(typeof(ArgumentCommand)),
|
||||||
|
new [] { "abc" },
|
||||||
|
new CommandInput(new [] { "arg", "cmd", "abc" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>())),
|
||||||
|
new ArgumentCommand { FirstArgument = "abc", Option = "option value" }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
|
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
|
||||||
{
|
{
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new DivideCommand(),
|
new DivideCommand(),
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("div")
|
GetCommandSchema(typeof(DivideCommand)),
|
||||||
);
|
new string[0],
|
||||||
|
new CommandInput(new[] { "div" })
|
||||||
|
));
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new DivideCommand(),
|
new DivideCommand(),
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("div", new[]
|
GetCommandSchema(typeof(DivideCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("D", "13")
|
new CommandInput(new[] { "div" }, new[]
|
||||||
})
|
{
|
||||||
);
|
new CommandOptionInput("D", "13")
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new ConcatCommand(),
|
new ConcatCommand(),
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("concat")
|
GetCommandSchema(typeof(ConcatCommand)),
|
||||||
);
|
new string[0],
|
||||||
|
new CommandInput(new[] { "concat" })
|
||||||
|
));
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new ConcatCommand(),
|
new ConcatCommand(),
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
new CommandCandidate(
|
||||||
new CommandInput("concat", new[]
|
GetCommandSchema(typeof(ConcatCommand)),
|
||||||
{
|
new string[0],
|
||||||
new CommandOptionInput("s", "_")
|
new CommandInput(new[] { "concat" }, new[]
|
||||||
})
|
{
|
||||||
);
|
new CommandOptionInput("s", "_")
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
// Missing required positional argument
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new ArgumentCommand(),
|
||||||
|
new CommandCandidate(
|
||||||
|
GetCommandSchema(typeof(ArgumentCommand)),
|
||||||
|
new string[0],
|
||||||
|
new CommandInput(new string[0], new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Incorrect data type in list
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new ArgumentCommand(),
|
||||||
|
new CommandCandidate(
|
||||||
|
GetCommandSchema(typeof(ArgumentCommand)),
|
||||||
|
new []{ "abc", "123", "invalid" },
|
||||||
|
new CommandInput(new [] { "arg", "cmd", "abc", "123", "invalid" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extraneous unused arguments
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new SimpleArgumentCommand(),
|
||||||
|
new CommandCandidate(
|
||||||
|
GetCommandSchema(typeof(SimpleArgumentCommand)),
|
||||||
|
new []{ "abc", "123", "unused" },
|
||||||
|
new CommandInput(new [] { "arg", "cmd2", "abc", "123", "unused" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
|
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
|
||||||
public void InitializeCommand_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput,
|
public void InitializeCommand_Test(ICommand command, CommandCandidate commandCandidate,
|
||||||
ICommand expectedCommand)
|
ICommand expectedCommand)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var initializer = new CommandInitializer();
|
var initializer = new CommandInitializer();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
initializer.InitializeCommand(command, commandSchema, commandInput);
|
initializer.InitializeCommand(command, commandCandidate);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
|
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
|
||||||
@@ -161,13 +232,13 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
|
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
|
||||||
public void InitializeCommand_Negative_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
|
public void InitializeCommand_Negative_Test(ICommand command, CommandCandidate commandCandidate)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var initializer = new CommandInitializer();
|
var initializer = new CommandInitializer();
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
initializer.Invoking(i => i.InitializeCommand(command, commandSchema, commandInput))
|
initializer.Invoking(i => i.InitializeCommand(command, commandCandidate))
|
||||||
.Should().ThrowExactly<CliFxException>();
|
.Should().ThrowExactly<CliFxException>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using NUnit.Framework;
|
|||||||
namespace CliFx.Tests.Services
|
namespace CliFx.Tests.Services
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class CommandOptionInputConverterTests
|
public class CommandInputConverterTests
|
||||||
{
|
{
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput()
|
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput()
|
||||||
{
|
{
|
||||||
@@ -298,7 +298,7 @@ namespace CliFx.Tests.Services
|
|||||||
object expectedConvertedValue)
|
object expectedConvertedValue)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var converter = new CommandOptionInputConverter();
|
var converter = new CommandInputConverter();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var convertedValue = converter.ConvertOptionInput(optionInput, targetType);
|
var convertedValue = converter.ConvertOptionInput(optionInput, targetType);
|
||||||
@@ -313,7 +313,7 @@ namespace CliFx.Tests.Services
|
|||||||
public void ConvertOptionInput_Negative_Test(CommandOptionInput optionInput, Type targetType)
|
public void ConvertOptionInput_Negative_Test(CommandOptionInput optionInput, Type targetType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var converter = new CommandOptionInputConverter();
|
var converter = new CommandInputConverter();
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType))
|
converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType))
|
||||||
@@ -158,13 +158,13 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "command" },
|
new[] { "command" },
|
||||||
new CommandInput("command"),
|
new CommandInput(new []{ "command" }),
|
||||||
new EmptyEnvironmentVariablesProviderStub()
|
new EmptyEnvironmentVariablesProviderStub()
|
||||||
);
|
);
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "command", "--option", "value" },
|
new[] { "command", "--option", "value" },
|
||||||
new CommandInput("command", new[]
|
new CommandInput(new []{ "command" }, new[]
|
||||||
{
|
{
|
||||||
new CommandOptionInput("option", "value")
|
new CommandOptionInput("option", "value")
|
||||||
}),
|
}),
|
||||||
@@ -173,13 +173,13 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "long", "command", "name" },
|
new[] { "long", "command", "name" },
|
||||||
new CommandInput("long command name"),
|
new CommandInput(new []{ "long", "command", "name"}),
|
||||||
new EmptyEnvironmentVariablesProviderStub()
|
new EmptyEnvironmentVariablesProviderStub()
|
||||||
);
|
);
|
||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "long", "command", "name", "--option", "value" },
|
new[] { "long", "command", "name", "--option", "value" },
|
||||||
new CommandInput("long command name", new[]
|
new CommandInput(new []{ "long", "command", "name" }, new[]
|
||||||
{
|
{
|
||||||
new CommandOptionInput("option", "value")
|
new CommandOptionInput("option", "value")
|
||||||
}),
|
}),
|
||||||
@@ -188,7 +188,7 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "[debug]" },
|
new[] { "[debug]" },
|
||||||
new CommandInput(null,
|
new CommandInput(new string[0],
|
||||||
new[] { "debug" },
|
new[] { "debug" },
|
||||||
new CommandOptionInput[0]),
|
new CommandOptionInput[0]),
|
||||||
new EmptyEnvironmentVariablesProviderStub()
|
new EmptyEnvironmentVariablesProviderStub()
|
||||||
@@ -196,7 +196,7 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "[debug]", "[preview]" },
|
new[] { "[debug]", "[preview]" },
|
||||||
new CommandInput(null,
|
new CommandInput(new string[0],
|
||||||
new[] { "debug", "preview" },
|
new[] { "debug", "preview" },
|
||||||
new CommandOptionInput[0]),
|
new CommandOptionInput[0]),
|
||||||
new EmptyEnvironmentVariablesProviderStub()
|
new EmptyEnvironmentVariablesProviderStub()
|
||||||
@@ -204,7 +204,7 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "[debug]", "[preview]", "-o", "value" },
|
new[] { "[debug]", "[preview]", "-o", "value" },
|
||||||
new CommandInput(null,
|
new CommandInput(new string[0],
|
||||||
new[] { "debug", "preview" },
|
new[] { "debug", "preview" },
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
@@ -215,7 +215,7 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "command", "[debug]", "[preview]", "-o", "value" },
|
new[] { "command", "[debug]", "[preview]", "-o", "value" },
|
||||||
new CommandInput("command",
|
new CommandInput(new []{"command"},
|
||||||
new[] { "debug", "preview" },
|
new[] { "debug", "preview" },
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
@@ -226,7 +226,7 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(
|
yield return new TestCaseData(
|
||||||
new[] { "command", "[debug]", "[preview]", "-o", "value" },
|
new[] { "command", "[debug]", "[preview]", "-o", "value" },
|
||||||
new CommandInput("command",
|
new CommandInput(new []{ "command"},
|
||||||
new[] { "debug", "preview" },
|
new[] { "debug", "preview" },
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace CliFx.Tests.Services
|
|||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
|
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
|
||||||
new[]
|
new CommandArgumentSchema[0], new[]
|
||||||
{
|
{
|
||||||
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
|
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
|
||||||
"dividend", 'D', true, "The number to divide.", null),
|
"dividend", 'D', true, "The number to divide.", null),
|
||||||
@@ -27,6 +27,7 @@ namespace CliFx.Tests.Services
|
|||||||
"divisor", 'd', true, "The number to divide by.", null)
|
"divisor", 'd', true, "The number to divide by.", null)
|
||||||
}),
|
}),
|
||||||
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
|
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
|
||||||
|
new CommandArgumentSchema[0],
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
|
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
|
||||||
@@ -35,6 +36,7 @@ namespace CliFx.Tests.Services
|
|||||||
null, 's', false, "String separator.", null)
|
null, 's', false, "String separator.", null)
|
||||||
}),
|
}),
|
||||||
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
|
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
|
||||||
|
new CommandArgumentSchema[0],
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
|
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
|
||||||
@@ -48,7 +50,7 @@ namespace CliFx.Tests.Services
|
|||||||
new[] { typeof(HelloWorldDefaultCommand) },
|
new[] { typeof(HelloWorldDefaultCommand) },
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0])
|
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandArgumentSchema[0], new CommandOptionSchema[0])
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -62,37 +64,192 @@ namespace CliFx.Tests.Services
|
|||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
yield return new TestCaseData(new object[]
|
||||||
{
|
{
|
||||||
new[] {typeof(NonImplementedCommand)}
|
new[] { typeof(NonImplementedCommand) }
|
||||||
});
|
});
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
yield return new TestCaseData(new object[]
|
||||||
{
|
{
|
||||||
new[] {typeof(NonAnnotatedCommand)}
|
new[] { typeof(NonAnnotatedCommand) }
|
||||||
});
|
});
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
yield return new TestCaseData(new object[]
|
||||||
{
|
{
|
||||||
new[] {typeof(DuplicateOptionNamesCommand)}
|
new[] { typeof(DuplicateOptionNamesCommand) }
|
||||||
});
|
});
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
yield return new TestCaseData(new object[]
|
||||||
{
|
{
|
||||||
new[] {typeof(DuplicateOptionShortNamesCommand)}
|
new[] { typeof(DuplicateOptionShortNamesCommand) }
|
||||||
});
|
});
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
yield return new TestCaseData(new object[]
|
||||||
{
|
{
|
||||||
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}
|
new[] { typeof(ExceptionCommand), typeof(CommandExceptionCommand) }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> GetTestCases_GetTargetCommandSchema_Positive()
|
||||||
|
{
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "command1", null, null, null),
|
||||||
|
new CommandSchema(null, "command2", null, null, null),
|
||||||
|
new CommandSchema(null, "command3", null, null, null)
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "command1", "argument1", "argument2" }),
|
||||||
|
new[] { "argument1", "argument2" },
|
||||||
|
"command1"
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "", null, null, null),
|
||||||
|
new CommandSchema(null, "command1", null, null, null),
|
||||||
|
new CommandSchema(null, "command2", null, null, null),
|
||||||
|
new CommandSchema(null, "command3", null, null, null)
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "argument1", "argument2" }),
|
||||||
|
new[] { "argument1", "argument2" },
|
||||||
|
""
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "command1 subcommand1", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "command1", "subcommand1", "argument1" }),
|
||||||
|
new[] { "argument1" },
|
||||||
|
"command1 subcommand1"
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "", null, null, null),
|
||||||
|
new CommandSchema(null, "a", null, null, null),
|
||||||
|
new CommandSchema(null, "a b", null, null, null),
|
||||||
|
new CommandSchema(null, "a b c", null, null, null),
|
||||||
|
new CommandSchema(null, "b", null, null, null),
|
||||||
|
new CommandSchema(null, "b c", null, null, null),
|
||||||
|
new CommandSchema(null, "c", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "a", "b", "d" }),
|
||||||
|
new[] { "d" },
|
||||||
|
"a b"
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "", null, null, null),
|
||||||
|
new CommandSchema(null, "a", null, null, null),
|
||||||
|
new CommandSchema(null, "a b", null, null, null),
|
||||||
|
new CommandSchema(null, "a b c", null, null, null),
|
||||||
|
new CommandSchema(null, "b", null, null, null),
|
||||||
|
new CommandSchema(null, "b c", null, null, null),
|
||||||
|
new CommandSchema(null, "c", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "a", "b", "c", "d" }),
|
||||||
|
new[] { "d" },
|
||||||
|
"a b c"
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "", null, null, null),
|
||||||
|
new CommandSchema(null, "a", null, null, null),
|
||||||
|
new CommandSchema(null, "a b", null, null, null),
|
||||||
|
new CommandSchema(null, "a b c", null, null, null),
|
||||||
|
new CommandSchema(null, "b", null, null, null),
|
||||||
|
new CommandSchema(null, "b c", null, null, null),
|
||||||
|
new CommandSchema(null, "c", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "b", "c" }),
|
||||||
|
new string[0],
|
||||||
|
"b c"
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "", null, null, null),
|
||||||
|
new CommandSchema(null, "a", null, null, null),
|
||||||
|
new CommandSchema(null, "a b", null, null, null),
|
||||||
|
new CommandSchema(null, "a b c", null, null, null),
|
||||||
|
new CommandSchema(null, "b", null, null, null),
|
||||||
|
new CommandSchema(null, "b c", null, null, null),
|
||||||
|
new CommandSchema(null, "c", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "d", "a", "b"}),
|
||||||
|
new[] { "d", "a", "b" },
|
||||||
|
""
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "", null, null, null),
|
||||||
|
new CommandSchema(null, "a", null, null, null),
|
||||||
|
new CommandSchema(null, "a b", null, null, null),
|
||||||
|
new CommandSchema(null, "a b c", null, null, null),
|
||||||
|
new CommandSchema(null, "b", null, null, null),
|
||||||
|
new CommandSchema(null, "b c", null, null, null),
|
||||||
|
new CommandSchema(null, "c", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "a", "b c", "d" }),
|
||||||
|
new[] { "b c", "d" },
|
||||||
|
"a"
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "", null, null, null),
|
||||||
|
new CommandSchema(null, "a", null, null, null),
|
||||||
|
new CommandSchema(null, "a b", null, null, null),
|
||||||
|
new CommandSchema(null, "a b c", null, null, null),
|
||||||
|
new CommandSchema(null, "b", null, null, null),
|
||||||
|
new CommandSchema(null, "b c", null, null, null),
|
||||||
|
new CommandSchema(null, "c", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "a b", "c", "d" }),
|
||||||
|
new[] { "a b", "c", "d" },
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> GetTestCases_GetTargetCommandSchema_Negative()
|
||||||
|
{
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "command1", null, null, null),
|
||||||
|
new CommandSchema(null, "command2", null, null, null),
|
||||||
|
new CommandSchema(null, "command3", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "command4", "argument1" })
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "command1", null, null, null),
|
||||||
|
new CommandSchema(null, "command2", null, null, null),
|
||||||
|
new CommandSchema(null, "command3", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "argument1" })
|
||||||
|
);
|
||||||
|
yield return new TestCaseData(
|
||||||
|
new []
|
||||||
|
{
|
||||||
|
new CommandSchema(null, "command1 subcommand1", null, null, null),
|
||||||
|
},
|
||||||
|
new CommandInput(new[] { "command1", "argument1" })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
|
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
|
||||||
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes,
|
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes,
|
||||||
IReadOnlyList<CommandSchema> expectedCommandSchemas)
|
IReadOnlyList<CommandSchema> expectedCommandSchemas)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var commandSchemaResolver = new CommandSchemaResolver();
|
var commandSchemaResolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes);
|
var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes);
|
||||||
@@ -106,11 +263,44 @@ namespace CliFx.Tests.Services
|
|||||||
public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes)
|
public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var resolver = new CommandSchemaResolver();
|
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
|
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
|
||||||
.Should().ThrowExactly<CliFxException>();
|
.Should().ThrowExactly<CliFxException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(GetTestCases_GetTargetCommandSchema_Positive))]
|
||||||
|
public void GetTargetCommandSchema_Positive_Test(IReadOnlyList<CommandSchema> availableCommandSchemas,
|
||||||
|
CommandInput commandInput,
|
||||||
|
IReadOnlyList<string> expectedPositionalArguments,
|
||||||
|
string expectedCommandSchemaName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var commandCandidate = resolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
commandCandidate.Should().NotBeNull();
|
||||||
|
commandCandidate.PositionalArgumentsInput.Should().BeEquivalentTo(expectedPositionalArguments);
|
||||||
|
commandCandidate.Schema.Name.Should().Be(expectedCommandSchemaName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCaseSource(nameof(GetTestCases_GetTargetCommandSchema_Negative))]
|
||||||
|
public void GetTargetCommandSchema_Negative_Test(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandInput commandInput)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var commandCandidate = resolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
commandCandidate.Should().BeNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ namespace CliFx.Tests.Services
|
|||||||
public class DelegateCommandFactoryTests
|
public class DelegateCommandFactoryTests
|
||||||
{
|
{
|
||||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
private static CommandSchema GetCommandSchema(Type commandType) =>
|
||||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] {commandType}).Single();
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace CliFx.Tests.Services
|
|||||||
{
|
{
|
||||||
private static HelpTextSource CreateHelpTextSource(IReadOnlyList<Type> availableCommandTypes, Type targetCommandType)
|
private static HelpTextSource CreateHelpTextSource(IReadOnlyList<Type> availableCommandTypes, Type targetCommandType)
|
||||||
{
|
{
|
||||||
var commandSchemaResolver = new CommandSchemaResolver();
|
var commandSchemaResolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
|
||||||
|
|
||||||
var applicationMetadata = new ApplicationMetadata("TestApp", "testapp", "1.0", null);
|
var applicationMetadata = new ApplicationMetadata("TestApp", "testapp", "1.0", null);
|
||||||
var availableCommandSchemas = commandSchemaResolver.GetCommandSchemas(availableCommandTypes);
|
var availableCommandSchemas = commandSchemaResolver.GetCommandSchemas(availableCommandTypes);
|
||||||
@@ -85,6 +85,27 @@ namespace CliFx.Tests.Services
|
|||||||
"-h|--help", "Shows help text."
|
"-h|--help", "Shows help text."
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
yield return new TestCaseData(
|
||||||
|
CreateHelpTextSource(
|
||||||
|
new[] {typeof(ArgumentCommand)},
|
||||||
|
typeof(ArgumentCommand)),
|
||||||
|
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Description",
|
||||||
|
"Command using positional arguments",
|
||||||
|
"Usage",
|
||||||
|
"arg cmd", "<first>", "[<secondargument>]", "[<third list>]", "[options]",
|
||||||
|
"Arguments",
|
||||||
|
"* first",
|
||||||
|
"secondargument",
|
||||||
|
"third list", "A list of numbers",
|
||||||
|
"Options",
|
||||||
|
"-o|--option",
|
||||||
|
"-h|--help", "Shows help text."
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
25
CliFx.Tests/TestCommands/ArgumentCommand.cs
Normal file
25
CliFx.Tests/TestCommands/ArgumentCommand.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.TestCommands
|
||||||
|
{
|
||||||
|
[Command("arg cmd", Description = "Command using positional arguments")]
|
||||||
|
public class ArgumentCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandArgument(0, IsRequired = true, Name = "first")]
|
||||||
|
public string? FirstArgument { get; set; }
|
||||||
|
|
||||||
|
[CommandArgument(10)]
|
||||||
|
public int? SecondArgument { get; set; }
|
||||||
|
|
||||||
|
[CommandArgument(20, Description = "A list of numbers", Name = "third list")]
|
||||||
|
public IEnumerable<int> ThirdArguments { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("option", 'o')]
|
||||||
|
public string Option { get; set; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
CliFx.Tests/TestCommands/SimpleArgumentCommand.cs
Normal file
21
CliFx.Tests/TestCommands/SimpleArgumentCommand.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Services;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.TestCommands
|
||||||
|
{
|
||||||
|
[Command("arg cmd2", Description = "Command using positional arguments")]
|
||||||
|
public class SimpleArgumentCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandArgument(0, IsRequired = true, Name = "first")]
|
||||||
|
public string? FirstArgument { get; set; }
|
||||||
|
|
||||||
|
[CommandArgument(10)]
|
||||||
|
public int? SecondArgument { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("option", 'o')]
|
||||||
|
public string Option { get; set; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
CliFx/Attributes/CommandArgumentAttribute.cs
Normal file
42
CliFx/Attributes/CommandArgumentAttribute.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Annotates a property that defines a command argument.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class CommandArgumentAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the argument, which is used in help text.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the argument is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Argument description, which is used in help text.
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ordering of the argument. Lower values will appear before higher values.
|
||||||
|
/// <remarks>
|
||||||
|
/// Two arguments of the same command cannot have the same <see cref="Order"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// </summary>
|
||||||
|
public int Order { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandArgumentAttribute"/> with a given order.
|
||||||
|
/// </summary>
|
||||||
|
public CommandArgumentAttribute(int order)
|
||||||
|
{
|
||||||
|
Order = order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Internal;
|
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
using CliFx.Services;
|
using CliFx.Services;
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ namespace CliFx
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Render command name
|
// Render command name
|
||||||
_console.Output.WriteLine($"Command name: {commandInput.CommandName}");
|
_console.Output.WriteLine($"Arguments: {string.Join(" ", commandInput.Arguments)}");
|
||||||
_console.Output.WriteLine();
|
_console.Output.WriteLine();
|
||||||
|
|
||||||
// Render directives
|
// Render directives
|
||||||
@@ -103,7 +101,7 @@ namespace CliFx
|
|||||||
private int? HandleVersionOption(CommandInput commandInput)
|
private int? HandleVersionOption(CommandInput commandInput)
|
||||||
{
|
{
|
||||||
// Version should be rendered if it was requested on a default command
|
// Version should be rendered if it was requested on a default command
|
||||||
var shouldRenderVersion = !commandInput.IsCommandSpecified() && commandInput.IsVersionOptionSpecified();
|
var shouldRenderVersion = !commandInput.HasArguments() && commandInput.IsVersionOptionSpecified();
|
||||||
|
|
||||||
// If shouldn't render version, pass execution to the next handler
|
// If shouldn't render version, pass execution to the next handler
|
||||||
if (!shouldRenderVersion)
|
if (!shouldRenderVersion)
|
||||||
@@ -117,10 +115,10 @@ namespace CliFx
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int? HandleHelpOption(CommandInput commandInput,
|
private int? HandleHelpOption(CommandInput commandInput,
|
||||||
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema? targetCommandSchema)
|
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandCandidate? commandCandidate)
|
||||||
{
|
{
|
||||||
// Help should be rendered if it was requested, or when executing a command which isn't defined
|
// Help should be rendered if it was requested, or when executing a command which isn't defined
|
||||||
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || targetCommandSchema == null;
|
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || commandCandidate == null;
|
||||||
|
|
||||||
// If shouldn't render help, pass execution to the next handler
|
// If shouldn't render help, pass execution to the next handler
|
||||||
if (!shouldRenderHelp)
|
if (!shouldRenderHelp)
|
||||||
@@ -129,31 +127,22 @@ namespace CliFx
|
|||||||
// Keep track whether there was an error in the input
|
// Keep track whether there was an error in the input
|
||||||
var isError = false;
|
var isError = false;
|
||||||
|
|
||||||
// If target command isn't defined, find its contextual replacement
|
// Report error if no command matched the arguments
|
||||||
if (targetCommandSchema == null)
|
if (commandCandidate is null)
|
||||||
{
|
{
|
||||||
// If command was specified, inform the user that it's not defined
|
// If a command was specified, inform the user that the command is not defined
|
||||||
if (commandInput.IsCommandSpecified())
|
if (commandInput.HasArguments())
|
||||||
{
|
{
|
||||||
_console.WithForegroundColor(ConsoleColor.Red,
|
_console.WithForegroundColor(ConsoleColor.Red,
|
||||||
() => _console.Error.WriteLine($"Specified command [{commandInput.CommandName}] is not defined."));
|
() => _console.Error.WriteLine($"No command could be matched for input [{string.Join(" ", commandInput.Arguments)}]"));
|
||||||
|
|
||||||
isError = true;
|
isError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace target command with closest parent of specified command
|
commandCandidate = new CommandCandidate(CommandSchema.StubDefaultCommand, new string[0], commandInput);
|
||||||
targetCommandSchema = availableCommandSchemas.FindParent(commandInput.CommandName);
|
|
||||||
|
|
||||||
// If there's no parent, replace with stub default command
|
|
||||||
if (targetCommandSchema == null)
|
|
||||||
{
|
|
||||||
targetCommandSchema = CommandSchema.StubDefaultCommand;
|
|
||||||
availableCommandSchemas = availableCommandSchemas.Concat(CommandSchema.StubDefaultCommand).ToArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build help text source
|
// Build help text source
|
||||||
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema);
|
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, commandCandidate.Schema);
|
||||||
|
|
||||||
// Render help text
|
// Render help text
|
||||||
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
|
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
|
||||||
@@ -162,13 +151,18 @@ namespace CliFx
|
|||||||
return isError ? -1 : 0;
|
return isError ? -1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<int> HandleCommandExecutionAsync(CommandInput commandInput, CommandSchema targetCommandSchema)
|
private async ValueTask<int> HandleCommandExecutionAsync(CommandCandidate? commandCandidate)
|
||||||
{
|
{
|
||||||
// Create an instance of the command
|
if (commandCandidate is null)
|
||||||
var command = _commandFactory.CreateCommand(targetCommandSchema);
|
{
|
||||||
|
throw new ArgumentException("Cannot execute command because it was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
// Populate command with options according to its schema
|
// Create an instance of the command
|
||||||
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
|
var command = _commandFactory.CreateCommand(commandCandidate.Schema);
|
||||||
|
|
||||||
|
// Populate command with options and arguments according to its schema
|
||||||
|
_commandInitializer.InitializeCommand(command, commandCandidate);
|
||||||
|
|
||||||
// Execute command
|
// Execute command
|
||||||
await command.ExecuteAsync(_console);
|
await command.ExecuteAsync(_console);
|
||||||
@@ -189,15 +183,15 @@ namespace CliFx
|
|||||||
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
|
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
|
||||||
|
|
||||||
// Find command schema matching the name specified in the input
|
// Find command schema matching the name specified in the input
|
||||||
var targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName);
|
var commandCandidate = _commandSchemaResolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
|
||||||
|
|
||||||
// Chain handlers until the first one that produces an exit code
|
// Chain handlers until the first one that produces an exit code
|
||||||
return
|
return
|
||||||
await HandleDebugDirectiveAsync(commandInput) ??
|
await HandleDebugDirectiveAsync(commandInput) ??
|
||||||
HandlePreviewDirective(commandInput) ??
|
HandlePreviewDirective(commandInput) ??
|
||||||
HandleVersionOption(commandInput) ??
|
HandleVersionOption(commandInput) ??
|
||||||
HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ??
|
HandleHelpOption(commandInput, availableCommandSchemas, commandCandidate) ??
|
||||||
await HandleCommandExecutionAsync(commandInput, targetCommandSchema!);
|
await HandleCommandExecutionAsync(commandCandidate);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace CliFx
|
|||||||
private string? _description;
|
private string? _description;
|
||||||
private IConsole? _console;
|
private IConsole? _console;
|
||||||
private ICommandFactory? _commandFactory;
|
private ICommandFactory? _commandFactory;
|
||||||
private ICommandOptionInputConverter? _commandOptionInputConverter;
|
private ICommandInputConverter? _commandInputConverter;
|
||||||
private IEnvironmentVariablesProvider? _environmentVariablesProvider;
|
private IEnvironmentVariablesProvider? _environmentVariablesProvider;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -107,9 +107,9 @@ namespace CliFx
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
|
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandInputConverter converter)
|
||||||
{
|
{
|
||||||
_commandOptionInputConverter = converter;
|
_commandInputConverter = converter;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ namespace CliFx
|
|||||||
_versionText ??= GetDefaultVersionText() ?? "v1.0";
|
_versionText ??= GetDefaultVersionText() ?? "v1.0";
|
||||||
_console ??= new SystemConsole();
|
_console ??= new SystemConsole();
|
||||||
_commandFactory ??= new CommandFactory();
|
_commandFactory ??= new CommandFactory();
|
||||||
_commandOptionInputConverter ??= new CommandOptionInputConverter();
|
_commandInputConverter ??= new CommandInputConverter();
|
||||||
_environmentVariablesProvider ??= new EnvironmentVariablesProvider();
|
_environmentVariablesProvider ??= new EnvironmentVariablesProvider();
|
||||||
|
|
||||||
// Project parameters to expected types
|
// Project parameters to expected types
|
||||||
@@ -137,8 +137,8 @@ namespace CliFx
|
|||||||
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
|
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
|
||||||
|
|
||||||
return new CliApplication(metadata, configuration,
|
return new CliApplication(metadata, configuration,
|
||||||
_console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(),
|
_console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(new CommandArgumentSchemasValidator()),
|
||||||
_commandFactory, new CommandInitializer(_commandOptionInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
|
_commandFactory, new CommandInitializer(_commandInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ namespace CliFx
|
|||||||
ICliApplicationBuilder UseCommandFactory(ICommandFactory factory);
|
ICliApplicationBuilder UseCommandFactory(ICommandFactory factory);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures application to use specified implementation of <see cref="ICommandOptionInputConverter"/>.
|
/// Configures application to use specified implementation of <see cref="ICommandInputConverter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter);
|
ICliApplicationBuilder UseCommandOptionInputConverter(ICommandInputConverter converter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures application to use specified implementation of <see cref="IEnvironmentVariablesProvider"/>.
|
/// Configures application to use specified implementation of <see cref="IEnvironmentVariablesProvider"/>.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
namespace CliFx.Internal
|
namespace CliFx.Internal
|
||||||
{
|
{
|
||||||
@@ -66,5 +67,11 @@ namespace CliFx.Internal
|
|||||||
|
|
||||||
public static bool IsCollection(this Type type) =>
|
public static bool IsCollection(this Type type) =>
|
||||||
type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
|
type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
|
||||||
|
|
||||||
|
public static IOrderedEnumerable<CommandArgumentSchema> Ordered(this IEnumerable<CommandArgumentSchema> source)
|
||||||
|
{
|
||||||
|
return source
|
||||||
|
.OrderBy(a => a.Order);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
78
CliFx/Models/CommandArgumentSchema.cs
Normal file
78
CliFx/Models/CommandArgumentSchema.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Schema of a defined command argument.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandArgumentSchema
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Underlying property.
|
||||||
|
/// </summary>
|
||||||
|
public PropertyInfo Property { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Argument name used for help text.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the argument is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Argument description.
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Order of the argument.
|
||||||
|
/// </summary>
|
||||||
|
public int Order { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The display name of the argument. Returns <see cref="Name"/> if specified, otherwise the name of the underlying property.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName => !string.IsNullOrWhiteSpace(Name) ? Name! : Property.Name.ToLower(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandArgumentSchema"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandArgumentSchema(PropertyInfo property, string? name, bool isRequired, string? description, int order)
|
||||||
|
{
|
||||||
|
Property = property;
|
||||||
|
Name = name;
|
||||||
|
IsRequired = isRequired;
|
||||||
|
Description = description;
|
||||||
|
Order = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the string representation of the argument schema.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
if (!IsRequired)
|
||||||
|
{
|
||||||
|
sb.Append("[");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append("<");
|
||||||
|
sb.Append($"{DisplayName}");
|
||||||
|
sb.Append(">");
|
||||||
|
|
||||||
|
if (!IsRequired)
|
||||||
|
{
|
||||||
|
sb.Append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
CliFx/Models/CommandCandidate.cs
Normal file
35
CliFx/Models/CommandCandidate.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CliFx.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the target command and the input required for initializing the command.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandCandidate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The command schema of the target command.
|
||||||
|
/// </summary>
|
||||||
|
public CommandSchema Schema { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The positional arguments input for the command.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> PositionalArgumentsInput { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The command input for the command.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInput CommandInput { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes and instance of <see cref="CommandCandidate"/>
|
||||||
|
/// </summary>
|
||||||
|
public CommandCandidate(CommandSchema schema, IReadOnlyList<string> positionalArgumentsInput, CommandInput commandInput)
|
||||||
|
{
|
||||||
|
Schema = schema;
|
||||||
|
PositionalArgumentsInput = positionalArgumentsInput;
|
||||||
|
CommandInput = commandInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,9 @@ namespace CliFx.Models
|
|||||||
public partial class CommandInput
|
public partial class CommandInput
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specified command name.
|
/// Specified arguments.
|
||||||
/// Can be null if command was not specified.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CommandName { get; }
|
public IReadOnlyList<string> Arguments { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specified directives.
|
/// Specified directives.
|
||||||
@@ -33,10 +32,10 @@ namespace CliFx.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
|
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
|
||||||
IReadOnlyDictionary<string, string> environmentVariables)
|
IReadOnlyDictionary<string, string> environmentVariables)
|
||||||
{
|
{
|
||||||
CommandName = commandName;
|
Arguments = arguments;
|
||||||
Directives = directives;
|
Directives = directives;
|
||||||
Options = options;
|
Options = options;
|
||||||
EnvironmentVariables = environmentVariables;
|
EnvironmentVariables = environmentVariables;
|
||||||
@@ -45,24 +44,24 @@ namespace CliFx.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
|
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
|
||||||
: this(commandName, directives, options, EmptyEnvironmentVariables)
|
: this(arguments, directives, options, EmptyEnvironmentVariables)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
|
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
|
||||||
: this(commandName, EmptyDirectives, options, environmentVariables)
|
: this(arguments, EmptyDirectives, options, environmentVariables)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options)
|
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<CommandOptionInput> options)
|
||||||
: this(commandName, EmptyDirectives, options)
|
: this(arguments, EmptyDirectives, options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,15 +69,15 @@ namespace CliFx.Models
|
|||||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInput(IReadOnlyList<CommandOptionInput> options)
|
public CommandInput(IReadOnlyList<CommandOptionInput> options)
|
||||||
: this(null, options)
|
: this(new string[0], options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInput(string? commandName)
|
public CommandInput(IReadOnlyList<string> arguments)
|
||||||
: this(commandName, EmptyOptions)
|
: this(arguments, EmptyOptions)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +86,11 @@ namespace CliFx.Models
|
|||||||
{
|
{
|
||||||
var buffer = new StringBuilder();
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(CommandName))
|
foreach (var argument in Arguments)
|
||||||
buffer.Append(CommandName);
|
{
|
||||||
|
buffer.AppendIfNotEmpty(' ');
|
||||||
|
buffer.Append(argument);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var directive in Directives)
|
foreach (var directive in Directives)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,15 +30,21 @@ namespace CliFx.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<CommandOptionSchema> Options { get; }
|
public IReadOnlyList<CommandOptionSchema> Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command arguments.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<CommandArgumentSchema> Arguments { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandSchema"/>.
|
/// Initializes an instance of <see cref="CommandSchema"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandSchema(Type? type, string? name, string? description, IReadOnlyList<CommandOptionSchema> options)
|
public CommandSchema(Type type, string name, string description, IReadOnlyList<CommandArgumentSchema> arguments, IReadOnlyList<CommandOptionSchema> options)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
Name = name;
|
Name = name;
|
||||||
Description = description;
|
Description = description;
|
||||||
Options = options;
|
Options = options;
|
||||||
|
Arguments = arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -64,6 +70,6 @@ namespace CliFx.Models
|
|||||||
public partial class CommandSchema
|
public partial class CommandSchema
|
||||||
{
|
{
|
||||||
internal static CommandSchema StubDefaultCommand { get; } =
|
internal static CommandSchema StubDefaultCommand { get; } =
|
||||||
new CommandSchema(null, null, null, new CommandOptionSchema[0]);
|
new CommandSchema(null, null, null, new CommandArgumentSchema[0], new CommandOptionSchema[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace CliFx.Models
|
namespace CliFx.Models
|
||||||
{
|
{
|
||||||
@@ -90,7 +91,7 @@ namespace CliFx.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether a command was specified in the input.
|
/// Gets whether a command was specified in the input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsCommandSpecified(this CommandInput commandInput) => !string.IsNullOrWhiteSpace(commandInput.CommandName);
|
public static bool HasArguments(this CommandInput commandInput) => commandInput.Arguments.Any();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether debug directive was specified in the input.
|
/// Gets whether debug directive was specified in the input.
|
||||||
|
|||||||
92
CliFx/Services/CommandArgumentSchemasValidator.cs
Normal file
92
CliFx/Services/CommandArgumentSchemasValidator.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Internal;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class CommandArgumentSchemasValidator : ICommandArgumentSchemasValidator
|
||||||
|
{
|
||||||
|
private bool IsEnumerableArgument(CommandArgumentSchema schema)
|
||||||
|
{
|
||||||
|
return schema.Property.PropertyType != typeof(string) && schema.Property.PropertyType.GetEnumerableUnderlyingType() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<ValidationError> ValidateArgumentSchemas(IReadOnlyCollection<CommandArgumentSchema> commandArgumentSchemas)
|
||||||
|
{
|
||||||
|
if (commandArgumentSchemas.Count == 0)
|
||||||
|
{
|
||||||
|
// No validation needed
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure there are no arguments with the same name
|
||||||
|
var duplicateNameGroups = commandArgumentSchemas
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
|
||||||
|
.GroupBy(x => x.Name)
|
||||||
|
.Where(x => x.Count() > 1);
|
||||||
|
foreach (var schema in duplicateNameGroups)
|
||||||
|
{
|
||||||
|
yield return new ValidationError($"Multiple arguments with same name: \"{schema.Key}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the order of all properties are distinct
|
||||||
|
var duplicateOrderGroups = commandArgumentSchemas
|
||||||
|
.GroupBy(x => x.Order)
|
||||||
|
.Where(x => x.Count() > 1);
|
||||||
|
foreach (var schema in duplicateOrderGroups)
|
||||||
|
{
|
||||||
|
yield return new ValidationError($"Multiple arguments with the same order: \"{schema.Key}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
var enumerableArguments = commandArgumentSchemas
|
||||||
|
.Where(IsEnumerableArgument)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Verify that no more than one enumerable argument exists
|
||||||
|
if (enumerableArguments.Count > 1)
|
||||||
|
{
|
||||||
|
yield return new ValidationError($"Multiple sequence arguments found; only one is supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an enumerable argument exists, ensure that it has the highest order
|
||||||
|
if (enumerableArguments.Count == 1)
|
||||||
|
{
|
||||||
|
if (enumerableArguments.Single().Order != commandArgumentSchemas.Max(x => x.Order))
|
||||||
|
{
|
||||||
|
yield return new ValidationError($"A sequence argument was defined with a lower order than another argument; the sequence argument must have the highest order (appear last).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that all required arguments appear before optional arguments
|
||||||
|
if (commandArgumentSchemas.Any(x => x.IsRequired) && commandArgumentSchemas.Any(x => !x.IsRequired) &&
|
||||||
|
commandArgumentSchemas.Where(x => x.IsRequired).Max(x => x.Order) > commandArgumentSchemas.Where(x => !x.IsRequired).Min(x => x.Order))
|
||||||
|
{
|
||||||
|
yield return new ValidationError("One or more required arguments appear after optional arguments. Required arguments must appear before (i.e. have lower order than) optional arguments.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a failed validation.
|
||||||
|
/// </summary>
|
||||||
|
public class ValidationError
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="ValidationError"/> with a message.
|
||||||
|
/// </summary>
|
||||||
|
public ValidationError(string message)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The error message for the failed validation.
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
@@ -10,15 +12,15 @@ namespace CliFx.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommandInitializer : ICommandInitializer
|
public class CommandInitializer : ICommandInitializer
|
||||||
{
|
{
|
||||||
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
|
private readonly ICommandInputConverter _commandInputConverter;
|
||||||
private readonly IEnvironmentVariablesParser _environmentVariablesParser;
|
private readonly IEnvironmentVariablesParser _environmentVariablesParser;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter, IEnvironmentVariablesParser environmentVariablesParser)
|
public CommandInitializer(ICommandInputConverter commandInputConverter, IEnvironmentVariablesParser environmentVariablesParser)
|
||||||
{
|
{
|
||||||
_commandOptionInputConverter = commandOptionInputConverter;
|
_commandInputConverter = commandInputConverter;
|
||||||
_environmentVariablesParser = environmentVariablesParser;
|
_environmentVariablesParser = environmentVariablesParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ namespace CliFx.Services
|
|||||||
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInitializer(IEnvironmentVariablesParser environmentVariablesParser)
|
public CommandInitializer(IEnvironmentVariablesParser environmentVariablesParser)
|
||||||
: this(new CommandOptionInputConverter(), environmentVariablesParser)
|
: this(new CommandInputConverter(), environmentVariablesParser)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,43 +36,47 @@ namespace CliFx.Services
|
|||||||
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
/// Initializes an instance of <see cref="CommandInitializer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandInitializer()
|
public CommandInitializer()
|
||||||
: this(new CommandOptionInputConverter(), new EnvironmentVariablesParser())
|
: this(new CommandInputConverter(), new EnvironmentVariablesParser())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private void InitializeCommandOptions(ICommand command, CommandCandidate commandCandidate)
|
||||||
public void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
|
|
||||||
{
|
{
|
||||||
|
if (commandCandidate.Schema is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot initialize command without a schema.");
|
||||||
|
}
|
||||||
|
|
||||||
// Keep track of unset required options to report an error at a later stage
|
// Keep track of unset required options to report an error at a later stage
|
||||||
var unsetRequiredOptions = commandSchema.Options.Where(o => o.IsRequired).ToList();
|
var unsetRequiredOptions = commandCandidate.Schema.Options.Where(o => o.IsRequired).ToList();
|
||||||
|
|
||||||
//Set command options
|
//Set command options
|
||||||
foreach (var optionSchema in commandSchema.Options)
|
foreach (var optionSchema in commandCandidate.Schema.Options)
|
||||||
{
|
{
|
||||||
// Ignore special options that are not backed by a property
|
// Ignore special options that are not backed by a property
|
||||||
if (optionSchema.Property == null)
|
if (optionSchema.Property == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
//Find matching option input
|
//Find matching option input
|
||||||
var optionInput = commandInput.Options.FindByOptionSchema(optionSchema);
|
var optionInput = commandCandidate.CommandInput.Options.FindByOptionSchema(optionSchema);
|
||||||
|
|
||||||
//If no option input is available fall back to environment variable values
|
//If no option input is available fall back to environment variable values
|
||||||
if (optionInput == null && !string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariableName))
|
if (optionInput == null && !string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariableName))
|
||||||
{
|
{
|
||||||
var fallbackEnvironmentVariableExists = commandInput.EnvironmentVariables.ContainsKey(optionSchema.EnvironmentVariableName!);
|
var fallbackEnvironmentVariableExists = commandCandidate.CommandInput.EnvironmentVariables.ContainsKey(optionSchema.EnvironmentVariableName!);
|
||||||
|
|
||||||
//If no environment variable is found or there is no valid value for this option skip it
|
//If no environment variable is found or there is no valid value for this option skip it
|
||||||
if (!fallbackEnvironmentVariableExists || string.IsNullOrWhiteSpace(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!]))
|
if (!fallbackEnvironmentVariableExists || string.IsNullOrWhiteSpace(commandCandidate.CommandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!]))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!], optionSchema);
|
optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandCandidate.CommandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!], optionSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
//No fallback available and no option input was specified, skip option
|
//No fallback available and no option input was specified, skip option
|
||||||
if (optionInput == null)
|
if (optionInput == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var convertedValue = _commandOptionInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType);
|
var convertedValue = _commandInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType);
|
||||||
|
|
||||||
// Set value of the underlying property
|
// Set value of the underlying property
|
||||||
optionSchema.Property.SetValue(command, convertedValue);
|
optionSchema.Property.SetValue(command, convertedValue);
|
||||||
@@ -87,5 +93,57 @@ namespace CliFx.Services
|
|||||||
throw new CliFxException($"Some of the required options were not provided: {unsetRequiredOptionNames}.");
|
throw new CliFxException($"Some of the required options were not provided: {unsetRequiredOptionNames}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeCommandArguments(ICommand command, CommandCandidate commandCandidate)
|
||||||
|
{
|
||||||
|
if (commandCandidate.Schema is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot initialize command without a schema.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of unset required options to report an error at a later stage
|
||||||
|
var unsetRequiredArguments = commandCandidate.Schema.Arguments
|
||||||
|
.Where(o => o.IsRequired)
|
||||||
|
.ToList();
|
||||||
|
var orderedArgumentSchemas = commandCandidate.Schema.Arguments.Ordered();
|
||||||
|
var argumentIndex = 0;
|
||||||
|
|
||||||
|
foreach (var argumentSchema in orderedArgumentSchemas)
|
||||||
|
{
|
||||||
|
if (argumentIndex >= commandCandidate.PositionalArgumentsInput.Count)
|
||||||
|
{
|
||||||
|
// No more positional arguments left - remaining argument properties stay unset
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var convertedValue = _commandInputConverter.ConvertArgumentInput(commandCandidate.PositionalArgumentsInput, ref argumentIndex, argumentSchema.Property.PropertyType);
|
||||||
|
|
||||||
|
// Set value of underlying property
|
||||||
|
argumentSchema.Property.SetValue(command, convertedValue);
|
||||||
|
|
||||||
|
// Mark this required argument as set
|
||||||
|
if (argumentSchema.IsRequired)
|
||||||
|
unsetRequiredArguments.Remove(argumentSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw if there are remaining input arguments
|
||||||
|
if (argumentIndex < commandCandidate.PositionalArgumentsInput.Count)
|
||||||
|
{
|
||||||
|
throw new CliFxException($"Could not map the following arguments to command name or positional arguments: {commandCandidate.PositionalArgumentsInput.Skip(argumentIndex).JoinToString(", ")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw if any of the required arguments were not set
|
||||||
|
if (unsetRequiredArguments.Any())
|
||||||
|
{
|
||||||
|
throw new CliFxException($"One or more required arguments were not set: {unsetRequiredArguments.JoinToString(", ")}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void InitializeCommand(ICommand command, CommandCandidate commandCandidate)
|
||||||
|
{
|
||||||
|
InitializeCommandOptions(command, commandCandidate);
|
||||||
|
InitializeCommandArguments(command, commandCandidate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -9,28 +10,53 @@ using CliFx.Models;
|
|||||||
namespace CliFx.Services
|
namespace CliFx.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default implementation of <see cref="ICommandOptionInputConverter"/>.
|
/// Default implementation of <see cref="ICommandInputConverter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CommandOptionInputConverter : ICommandOptionInputConverter
|
public partial class CommandInputConverter : ICommandInputConverter
|
||||||
{
|
{
|
||||||
private readonly IFormatProvider _formatProvider;
|
private readonly IFormatProvider _formatProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandOptionInputConverter"/>.
|
/// Initializes an instance of <see cref="CommandInputConverter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandOptionInputConverter(IFormatProvider formatProvider)
|
public CommandInputConverter(IFormatProvider formatProvider)
|
||||||
{
|
{
|
||||||
_formatProvider = formatProvider;
|
_formatProvider = formatProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandOptionInputConverter"/>.
|
/// Initializes an instance of <see cref="CommandInputConverter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandOptionInputConverter()
|
public CommandInputConverter()
|
||||||
: this(CultureInfo.InvariantCulture)
|
: this(CultureInfo.InvariantCulture)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object? ConvertEnumerableValue(IReadOnlyList<string> values, Type enumerableUnderlyingType, Type targetType)
|
||||||
|
{
|
||||||
|
// Convert values to the underlying enumerable type and cast it to dynamic array
|
||||||
|
var convertedValues = values
|
||||||
|
.Select(v => ConvertValue(v, enumerableUnderlyingType))
|
||||||
|
.ToNonGenericArray(enumerableUnderlyingType);
|
||||||
|
|
||||||
|
// Get the type of produced array
|
||||||
|
var convertedValuesType = convertedValues.GetType();
|
||||||
|
|
||||||
|
// Try to assign the array (works for T[], IReadOnlyList<T>, IEnumerable<T>, etc)
|
||||||
|
if (targetType.IsAssignableFrom(convertedValuesType))
|
||||||
|
return convertedValues;
|
||||||
|
|
||||||
|
// Try to inject the array into the constructor (works for HashSet<T>, List<T>, etc)
|
||||||
|
var arrayConstructor = targetType.GetConstructor(new[] { convertedValuesType });
|
||||||
|
if (arrayConstructor != null)
|
||||||
|
return arrayConstructor.Invoke(new object[] { convertedValues });
|
||||||
|
|
||||||
|
// Throw if we can't find a way to convert the values
|
||||||
|
throw new CliFxException(
|
||||||
|
$"Can't convert a sequence of values [{values.JoinToString(", ")}] " +
|
||||||
|
$"to type [{targetType}].");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a single string value to specified target type.
|
/// Converts a single string value to specified target type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -118,17 +144,17 @@ namespace CliFx.Services
|
|||||||
// Has a constructor that accepts a single string
|
// Has a constructor that accepts a single string
|
||||||
var stringConstructor = GetStringConstructor(targetType);
|
var stringConstructor = GetStringConstructor(targetType);
|
||||||
if (stringConstructor != null)
|
if (stringConstructor != null)
|
||||||
return stringConstructor.Invoke(new object[] {value});
|
return stringConstructor.Invoke(new object[] { value });
|
||||||
|
|
||||||
// Has a static parse method that accepts a single string and a format provider
|
// Has a static parse method that accepts a single string and a format provider
|
||||||
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
|
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
|
||||||
if (parseMethodWithFormatProvider != null)
|
if (parseMethodWithFormatProvider != null)
|
||||||
return parseMethodWithFormatProvider.Invoke(null, new object[] {value, _formatProvider});
|
return parseMethodWithFormatProvider.Invoke(null, new object[] { value, _formatProvider });
|
||||||
|
|
||||||
// Has a static parse method that accepts a single string
|
// Has a static parse method that accepts a single string
|
||||||
var parseMethod = GetStaticParseMethod(targetType);
|
var parseMethod = GetStaticParseMethod(targetType);
|
||||||
if (parseMethod != null)
|
if (parseMethod != null)
|
||||||
return parseMethod.Invoke(null, new object[] {value});
|
return parseMethod.Invoke(null, new object[] { value });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -145,6 +171,24 @@ namespace CliFx.Services
|
|||||||
"This type is not among the list of types supported by this library.");
|
"This type is not among the list of types supported by this library.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual object? ConvertArgumentInput(IReadOnlyList<string> arguments, ref int currentIndex, Type targetType)
|
||||||
|
{
|
||||||
|
var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null;
|
||||||
|
if (enumerableUnderlyingType is null)
|
||||||
|
{
|
||||||
|
var argument = arguments[currentIndex];
|
||||||
|
currentIndex += 1;
|
||||||
|
return ConvertValue(argument, targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
var argumentSequence = arguments.Skip(currentIndex).ToList();
|
||||||
|
currentIndex = arguments.Count;
|
||||||
|
|
||||||
|
return ConvertEnumerableValue(argumentSequence, enumerableUnderlyingType, targetType);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
|
public virtual object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
|
||||||
{
|
{
|
||||||
@@ -170,32 +214,12 @@ namespace CliFx.Services
|
|||||||
// Convert to an enumerable type
|
// Convert to an enumerable type
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Convert values to the underlying enumerable type and cast it to dynamic array
|
return ConvertEnumerableValue(optionInput.Values, enumerableUnderlyingType, targetType);
|
||||||
var convertedValues = optionInput.Values
|
|
||||||
.Select(v => ConvertValue(v, enumerableUnderlyingType))
|
|
||||||
.ToNonGenericArray(enumerableUnderlyingType);
|
|
||||||
|
|
||||||
// Get the type of produced array
|
|
||||||
var convertedValuesType = convertedValues.GetType();
|
|
||||||
|
|
||||||
// Try to assign the array (works for T[], IReadOnlyList<T>, IEnumerable<T>, etc)
|
|
||||||
if (targetType.IsAssignableFrom(convertedValuesType))
|
|
||||||
return convertedValues;
|
|
||||||
|
|
||||||
// Try to inject the array into the constructor (works for HashSet<T>, List<T>, etc)
|
|
||||||
var arrayConstructor = targetType.GetConstructor(new[] {convertedValuesType});
|
|
||||||
if (arrayConstructor != null)
|
|
||||||
return arrayConstructor.Invoke(new object[] {convertedValues});
|
|
||||||
|
|
||||||
// Throw if we can't find a way to convert the values
|
|
||||||
throw new CliFxException(
|
|
||||||
$"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " +
|
|
||||||
$"to type [{targetType}].");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class CommandOptionInputConverter
|
public partial class CommandInputConverter
|
||||||
{
|
{
|
||||||
private static ConstructorInfo? GetStringConstructor(Type type) => type.GetConstructor(new[] {typeof(string)});
|
private static ConstructorInfo? GetStringConstructor(Type type) => type.GetConstructor(new[] {typeof(string)});
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using CliFx.Internal;
|
using CliFx.Internal;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ namespace CliFx.Services
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments)
|
public CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments)
|
||||||
{
|
{
|
||||||
var commandNameBuilder = new StringBuilder();
|
var arguments = new List<string>();
|
||||||
var directives = new List<string>();
|
var directives = new List<string>();
|
||||||
var optionsDic = new Dictionary<string, List<string>>();
|
var optionsDic = new Dictionary<string, List<string>>();
|
||||||
|
|
||||||
@@ -79,8 +78,7 @@ namespace CliFx.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
commandNameBuilder.AppendIfNotEmpty(' ');
|
arguments.Add(commandLineArgument);
|
||||||
commandNameBuilder.Append(commandLineArgument);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +89,11 @@ namespace CliFx.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null;
|
|
||||||
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
|
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
|
||||||
|
|
||||||
var environmentVariables = _environmentVariablesProvider.GetEnvironmentVariables();
|
var environmentVariables = _environmentVariablesProvider.GetEnvironmentVariables();
|
||||||
|
|
||||||
return new CommandInput(commandName, directives, options, environmentVariables);
|
return new CommandInput(arguments, directives, options, environmentVariables);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal;
|
||||||
@@ -14,6 +15,16 @@ namespace CliFx.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommandSchemaResolver : ICommandSchemaResolver
|
public class CommandSchemaResolver : ICommandSchemaResolver
|
||||||
{
|
{
|
||||||
|
private readonly ICommandArgumentSchemasValidator _commandArgumentSchemasValidator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <see cref="CommandSchemaResolver"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CommandSchemaResolver(ICommandArgumentSchemasValidator commandArgumentSchemasValidator)
|
||||||
|
{
|
||||||
|
_commandArgumentSchemasValidator = commandArgumentSchemasValidator;
|
||||||
|
}
|
||||||
|
|
||||||
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
|
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
|
||||||
{
|
{
|
||||||
var result = new List<CommandOptionSchema>();
|
var result = new List<CommandOptionSchema>();
|
||||||
@@ -67,6 +78,24 @@ namespace CliFx.Services
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<CommandArgumentSchema> GetCommandArgumentSchemas(Type commandType)
|
||||||
|
{
|
||||||
|
var argumentSchemas = commandType.GetProperties()
|
||||||
|
.Select(p => new { Property = p, Attribute = p.GetCustomAttribute<CommandArgumentAttribute>() })
|
||||||
|
.Where(a => a.Attribute != null)
|
||||||
|
.Select(a => new CommandArgumentSchema(a.Property, a.Attribute.Name, a.Attribute.IsRequired, a.Attribute.Description, a.Attribute.Order))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var validationErrors = _commandArgumentSchemasValidator.ValidateArgumentSchemas(argumentSchemas).ToList();
|
||||||
|
if (validationErrors.Any())
|
||||||
|
{
|
||||||
|
throw new CliFxException($"Command type [{commandType}] has invalid argument configuration:\n" +
|
||||||
|
$"{string.Join("\n", validationErrors.Select(v => v.Message))}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return argumentSchemas;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes)
|
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes)
|
||||||
{
|
{
|
||||||
@@ -108,11 +137,14 @@ namespace CliFx.Services
|
|||||||
// Get option schemas
|
// Get option schemas
|
||||||
var optionSchemas = GetCommandOptionSchemas(commandType);
|
var optionSchemas = GetCommandOptionSchemas(commandType);
|
||||||
|
|
||||||
|
// Get argument schemas
|
||||||
|
var argumentSchemas = GetCommandArgumentSchemas(commandType);
|
||||||
|
|
||||||
// Build command schema
|
// Build command schema
|
||||||
var commandSchema = new CommandSchema(commandType,
|
var commandSchema = new CommandSchema(commandType,
|
||||||
attribute.Name,
|
attribute.Name,
|
||||||
attribute.Description,
|
attribute.Description,
|
||||||
optionSchemas);
|
argumentSchemas, optionSchemas);
|
||||||
|
|
||||||
// Make sure there are no other commands with the same name
|
// Make sure there are no other commands with the same name
|
||||||
var existingCommandWithSameName = result
|
var existingCommandWithSameName = result
|
||||||
@@ -131,5 +163,31 @@ namespace CliFx.Services
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public CommandCandidate? GetTargetCommandSchema(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandInput commandInput)
|
||||||
|
{
|
||||||
|
// If no arguments are given, use the default command
|
||||||
|
CommandSchema targetSchema;
|
||||||
|
if (!commandInput.Arguments.Any())
|
||||||
|
{
|
||||||
|
targetSchema = availableCommandSchemas.FirstOrDefault(c => c.IsDefault());
|
||||||
|
return targetSchema is null ? null : new CommandCandidate(targetSchema, new string[0], commandInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arguments can be part of the a command name as long as they are single words, i.e. no whitespace characters
|
||||||
|
var longestPossibleCommandName = string.Join(" ", commandInput.Arguments.TakeWhile(arg => !Regex.IsMatch(arg, @"\s")));
|
||||||
|
|
||||||
|
// Find the longest matching schema
|
||||||
|
var orderedSchemas = availableCommandSchemas.OrderByDescending(x => x.Name?.Length);
|
||||||
|
targetSchema = orderedSchemas.FirstOrDefault(c => longestPossibleCommandName.StartsWith(c.Name ?? string.Empty, StringComparison.Ordinal))
|
||||||
|
?? availableCommandSchemas.FirstOrDefault(c => c.IsDefault());
|
||||||
|
|
||||||
|
// Get remaining positional arguments
|
||||||
|
var commandArgumentsCount = targetSchema?.Name?.Split(new []{ ' ' }, StringSplitOptions.RemoveEmptyEntries).Length ?? 0;
|
||||||
|
var positionalArguments = commandInput.Arguments.Skip(commandArgumentsCount).ToList();
|
||||||
|
|
||||||
|
return targetSchema is null ? null : new CommandCandidate(targetSchema, positionalArguments, commandInput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ namespace CliFx.Services
|
|||||||
var row = 0;
|
var row = 0;
|
||||||
|
|
||||||
// Get built-in option schemas (help and version)
|
// Get built-in option schemas (help and version)
|
||||||
var builtInOptionSchemas = new List<CommandOptionSchema> {CommandOptionSchema.HelpOption};
|
var builtInOptionSchemas = new List<CommandOptionSchema> { CommandOptionSchema.HelpOption };
|
||||||
if (source.TargetCommandSchema.IsDefault())
|
if (source.TargetCommandSchema.IsDefault())
|
||||||
builtInOptionSchemas.Add(CommandOptionSchema.VersionOption);
|
builtInOptionSchemas.Add(CommandOptionSchema.VersionOption);
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ namespace CliFx.Services
|
|||||||
// Description
|
// Description
|
||||||
if (!string.IsNullOrWhiteSpace(source.ApplicationMetadata.Description))
|
if (!string.IsNullOrWhiteSpace(source.ApplicationMetadata.Description))
|
||||||
{
|
{
|
||||||
Render(source.ApplicationMetadata.Description);
|
Render(source.ApplicationMetadata.Description!);
|
||||||
RenderNewLine();
|
RenderNewLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ namespace CliFx.Services
|
|||||||
|
|
||||||
// Description
|
// Description
|
||||||
RenderIndent();
|
RenderIndent();
|
||||||
Render(source.TargetCommandSchema.Description);
|
Render(source.TargetCommandSchema.Description!);
|
||||||
RenderNewLine();
|
RenderNewLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ namespace CliFx.Services
|
|||||||
if (!string.IsNullOrWhiteSpace(source.TargetCommandSchema.Name))
|
if (!string.IsNullOrWhiteSpace(source.TargetCommandSchema.Name))
|
||||||
{
|
{
|
||||||
Render(" ");
|
Render(" ");
|
||||||
RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan);
|
RenderWithColor(source.TargetCommandSchema.Name!, ConsoleColor.Cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Child command
|
// Child command
|
||||||
@@ -152,12 +152,69 @@ namespace CliFx.Services
|
|||||||
RenderWithColor("[command]", ConsoleColor.Cyan);
|
RenderWithColor("[command]", ConsoleColor.Cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Arguments
|
||||||
|
foreach (var argumentSchema in source.TargetCommandSchema.Arguments)
|
||||||
|
{
|
||||||
|
Render(" ");
|
||||||
|
if (!argumentSchema.IsRequired)
|
||||||
|
Render("[");
|
||||||
|
|
||||||
|
Render($"<{argumentSchema.DisplayName}>");
|
||||||
|
|
||||||
|
if (!argumentSchema.IsRequired)
|
||||||
|
Render("]");
|
||||||
|
}
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
Render(" ");
|
Render(" ");
|
||||||
RenderWithColor("[options]", ConsoleColor.White);
|
RenderWithColor("[options]", ConsoleColor.White);
|
||||||
RenderNewLine();
|
RenderNewLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RenderArguments()
|
||||||
|
{
|
||||||
|
// Do not render anything if the command has no arguments
|
||||||
|
if (source.TargetCommandSchema.Arguments.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Margin
|
||||||
|
RenderMargin();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RenderHeader("Arguments");
|
||||||
|
|
||||||
|
// Order arguments
|
||||||
|
var orderedArgumentSchemas = source.TargetCommandSchema.Arguments
|
||||||
|
.Ordered()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Arguments
|
||||||
|
foreach (var argumentSchema in orderedArgumentSchemas)
|
||||||
|
{
|
||||||
|
// Is required
|
||||||
|
if (argumentSchema.IsRequired)
|
||||||
|
{
|
||||||
|
RenderWithColor("* ", ConsoleColor.Red);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderIndent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short name
|
||||||
|
RenderWithColor($"{argumentSchema.DisplayName}", ConsoleColor.White);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (!string.IsNullOrWhiteSpace(argumentSchema.Description))
|
||||||
|
{
|
||||||
|
RenderColumnIndent();
|
||||||
|
Render(argumentSchema.Description!);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderNewLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RenderOptions()
|
void RenderOptions()
|
||||||
{
|
{
|
||||||
// Margin
|
// Margin
|
||||||
@@ -207,7 +264,7 @@ namespace CliFx.Services
|
|||||||
if (!string.IsNullOrWhiteSpace(optionSchema.Description))
|
if (!string.IsNullOrWhiteSpace(optionSchema.Description))
|
||||||
{
|
{
|
||||||
RenderColumnIndent();
|
RenderColumnIndent();
|
||||||
Render(optionSchema.Description);
|
Render(optionSchema.Description!);
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderNewLine();
|
RenderNewLine();
|
||||||
@@ -238,7 +295,7 @@ namespace CliFx.Services
|
|||||||
if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
|
if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
|
||||||
{
|
{
|
||||||
RenderColumnIndent();
|
RenderColumnIndent();
|
||||||
Render(childCommandSchema.Description);
|
Render(childCommandSchema.Description!);
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderNewLine();
|
RenderNewLine();
|
||||||
@@ -275,6 +332,7 @@ namespace CliFx.Services
|
|||||||
RenderApplicationInfo();
|
RenderApplicationInfo();
|
||||||
RenderDescription();
|
RenderDescription();
|
||||||
RenderUsage();
|
RenderUsage();
|
||||||
|
RenderArguments();
|
||||||
RenderOptions();
|
RenderOptions();
|
||||||
RenderChildCommands();
|
RenderChildCommands();
|
||||||
}
|
}
|
||||||
@@ -285,6 +343,6 @@ namespace CliFx.Services
|
|||||||
private static string? GetRelativeCommandName(CommandSchema commandSchema, CommandSchema parentCommandSchema) =>
|
private static string? GetRelativeCommandName(CommandSchema commandSchema, CommandSchema parentCommandSchema) =>
|
||||||
string.IsNullOrWhiteSpace(parentCommandSchema.Name) || string.IsNullOrWhiteSpace(commandSchema.Name)
|
string.IsNullOrWhiteSpace(parentCommandSchema.Name) || string.IsNullOrWhiteSpace(commandSchema.Name)
|
||||||
? commandSchema.Name
|
? commandSchema.Name
|
||||||
: commandSchema.Name.Substring(parentCommandSchema.Name.Length + 1);
|
: commandSchema.Name!.Substring(parentCommandSchema.Name!.Length + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
CliFx/Services/ICommandArgumentSchemasValidator.cs
Normal file
16
CliFx/Services/ICommandArgumentSchemasValidator.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using CliFx.Models;
|
||||||
|
|
||||||
|
namespace CliFx.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates command arguments.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandArgumentSchemasValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validate the given command arguments.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<ValidationError> ValidateArgumentSchemas(IReadOnlyCollection<CommandArgumentSchema> commandArgumentSchemas);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,6 @@ namespace CliFx.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populates an instance of <see cref="ICommand"/> with specified input according to specified schema.
|
/// Populates an instance of <see cref="ICommand"/> with specified input according to specified schema.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput);
|
void InitializeCommand(ICommand command, CommandCandidate commandCandidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using CliFx.Models;
|
using CliFx.Models;
|
||||||
|
|
||||||
namespace CliFx.Services
|
namespace CliFx.Services
|
||||||
@@ -6,11 +7,16 @@ namespace CliFx.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts input command options.
|
/// Converts input command options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICommandOptionInputConverter
|
public interface ICommandInputConverter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts an option to specified target type.
|
/// Converts an option to specified target type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType);
|
object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an argument to specified target type, using up arguments from the given enumerator.
|
||||||
|
/// </summary>
|
||||||
|
object? ConvertArgumentInput(IReadOnlyList<string> arguments, ref int currentIndex, Type targetType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,5 +13,10 @@ namespace CliFx.Services
|
|||||||
/// Resolves schemas of specified command types.
|
/// Resolves schemas of specified command types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes);
|
IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the target command schema. The target command is the most specific command that matches the unbound input arguments.
|
||||||
|
/// </summary>
|
||||||
|
CommandCandidate? GetTargetCommandSchema(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandInput commandInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user