From da79a016a5bc30b12337d70eb7b0b8474f701ba5 Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Sun, 2 Jun 2019 18:32:25 +0300 Subject: [PATCH] Add project files. --- CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj | 13 ++ CliFx.Tests.Dummy/Commands/AddCommand.cs | 25 ++++ CliFx.Tests.Dummy/Commands/DefaultCommand.cs | 31 +++++ CliFx.Tests.Dummy/Commands/LogCommand.cs | 25 ++++ CliFx.Tests.Dummy/Program.cs | 9 ++ CliFx.Tests/CliApplicationTests.cs | 33 +++++ CliFx.Tests/CliFx.Tests.csproj | 23 +++ CliFx.Tests/CommandOptionConverterTests.cs | 83 +++++++++++ CliFx.Tests/CommandOptionParserTests.cs | 139 +++++++++++++++++++ CliFx.Tests/CommandResolverTests.cs | 116 ++++++++++++++++ CliFx.Tests/DummyTests.cs | 32 +++++ CliFx.Tests/TestObjects/TestCommand.cs | 18 +++ CliFx.Tests/TestObjects/TestEnum.cs | 9 ++ CliFx.sln | 65 +++++++++ CliFx/Attributes/CommandAttribute.cs | 15 ++ CliFx/Attributes/CommandOptionAttribute.cs | 21 +++ CliFx/Attributes/DefaultCommandAttribute.cs | 9 ++ CliFx/CliApplication.cs | 45 ++++++ CliFx/CliFx.csproj | 8 ++ CliFx/Command.cs | 15 ++ CliFx/Exceptions/CommandResolveException.cs | 21 +++ CliFx/Extensions.cs | 12 ++ CliFx/ICliApplication.cs | 10 ++ CliFx/Internal/CommandOptionProperty.cs | 48 +++++++ CliFx/Internal/CommandType.cs | 52 +++++++ CliFx/Internal/Extensions.cs | 57 ++++++++ CliFx/Models/CommandOptionSet.cs | 37 +++++ CliFx/Models/ExitCode.cs | 26 ++++ CliFx/Services/CommandOptionConverter.cs | 56 ++++++++ CliFx/Services/CommandOptionParser.cs | 71 ++++++++++ CliFx/Services/CommandResolver.cs | 107 ++++++++++++++ CliFx/Services/Extensions.cs | 7 + CliFx/Services/ICommandOptionConverter.cs | 9 ++ CliFx/Services/ICommandOptionParser.cs | 10 ++ CliFx/Services/ICommandResolver.cs | 9 ++ CliFx/Services/ITypeProvider.cs | 10 ++ CliFx/Services/TypeProvider.cs | 34 +++++ 37 files changed, 1310 insertions(+) create mode 100644 CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj create mode 100644 CliFx.Tests.Dummy/Commands/AddCommand.cs create mode 100644 CliFx.Tests.Dummy/Commands/DefaultCommand.cs create mode 100644 CliFx.Tests.Dummy/Commands/LogCommand.cs create mode 100644 CliFx.Tests.Dummy/Program.cs create mode 100644 CliFx.Tests/CliApplicationTests.cs create mode 100644 CliFx.Tests/CliFx.Tests.csproj create mode 100644 CliFx.Tests/CommandOptionConverterTests.cs create mode 100644 CliFx.Tests/CommandOptionParserTests.cs create mode 100644 CliFx.Tests/CommandResolverTests.cs create mode 100644 CliFx.Tests/DummyTests.cs create mode 100644 CliFx.Tests/TestObjects/TestCommand.cs create mode 100644 CliFx.Tests/TestObjects/TestEnum.cs create mode 100644 CliFx.sln create mode 100644 CliFx/Attributes/CommandAttribute.cs create mode 100644 CliFx/Attributes/CommandOptionAttribute.cs create mode 100644 CliFx/Attributes/DefaultCommandAttribute.cs create mode 100644 CliFx/CliApplication.cs create mode 100644 CliFx/CliFx.csproj create mode 100644 CliFx/Command.cs create mode 100644 CliFx/Exceptions/CommandResolveException.cs create mode 100644 CliFx/Extensions.cs create mode 100644 CliFx/ICliApplication.cs create mode 100644 CliFx/Internal/CommandOptionProperty.cs create mode 100644 CliFx/Internal/CommandType.cs create mode 100644 CliFx/Internal/Extensions.cs create mode 100644 CliFx/Models/CommandOptionSet.cs create mode 100644 CliFx/Models/ExitCode.cs create mode 100644 CliFx/Services/CommandOptionConverter.cs create mode 100644 CliFx/Services/CommandOptionParser.cs create mode 100644 CliFx/Services/CommandResolver.cs create mode 100644 CliFx/Services/Extensions.cs create mode 100644 CliFx/Services/ICommandOptionConverter.cs create mode 100644 CliFx/Services/ICommandOptionParser.cs create mode 100644 CliFx/Services/ICommandResolver.cs create mode 100644 CliFx/Services/ITypeProvider.cs create mode 100644 CliFx/Services/TypeProvider.cs diff --git a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj new file mode 100644 index 0000000..dcae554 --- /dev/null +++ b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj @@ -0,0 +1,13 @@ + + + + Exe + net45 + latest + + + + + + + \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/AddCommand.cs b/CliFx.Tests.Dummy/Commands/AddCommand.cs new file mode 100644 index 0000000..e0649f5 --- /dev/null +++ b/CliFx.Tests.Dummy/Commands/AddCommand.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using CliFx.Attributes; +using CliFx.Models; + +namespace CliFx.Tests.Dummy.Commands +{ + [Command("add")] + public class AddCommand : Command + { + [CommandOption("a", IsRequired = true, Description = "Left operand.")] + public double A { get; set; } + + [CommandOption("b", IsRequired = true, Description = "Right operand.")] + public double B { get; set; } + + public override ExitCode Execute() + { + var result = A + B; + Console.WriteLine(result.ToString(CultureInfo.InvariantCulture)); + + return ExitCode.Success; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs new file mode 100644 index 0000000..69b7f46 --- /dev/null +++ b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs @@ -0,0 +1,31 @@ +using System; +using System.Text; +using CliFx.Attributes; +using CliFx.Models; + +namespace CliFx.Tests.Dummy.Commands +{ + [DefaultCommand] + public class DefaultCommand : Command + { + [CommandOption("target", ShortName = 't', Description = "Greeting target.")] + public string Target { get; set; } = "world"; + + [CommandOption("enthusiastic", ShortName = 'e', Description = "Whether the greeting should be enthusiastic.")] + public bool IsEnthusiastic { get; set; } + + public override ExitCode Execute() + { + var buffer = new StringBuilder(); + + buffer.Append("Hello ").Append(Target); + + if (IsEnthusiastic) + buffer.Append("!!!"); + + Console.WriteLine(buffer.ToString()); + + return ExitCode.Success; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs new file mode 100644 index 0000000..38532e6 --- /dev/null +++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using CliFx.Attributes; +using CliFx.Models; + +namespace CliFx.Tests.Dummy.Commands +{ + [Command("log")] + public class LogCommand : Command + { + [CommandOption("value", IsRequired = true, Description = "Value whose logarithm is to be found.")] + public double Value { get; set; } + + [CommandOption("base", Description = "Logarithm base.")] + public double Base { get; set; } = 10; + + public override ExitCode Execute() + { + var result = Math.Log(Value, Base); + Console.WriteLine(result.ToString(CultureInfo.InvariantCulture)); + + return ExitCode.Success; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Program.cs b/CliFx.Tests.Dummy/Program.cs new file mode 100644 index 0000000..c76bc5e --- /dev/null +++ b/CliFx.Tests.Dummy/Program.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace CliFx.Tests.Dummy +{ + public static class Program + { + public static Task Main(string[] args) => new CliApplication().RunAsync(args); + } +} \ No newline at end of file diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs new file mode 100644 index 0000000..e61e17e --- /dev/null +++ b/CliFx.Tests/CliApplicationTests.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx.Services; +using CliFx.Tests.TestObjects; +using Moq; +using NUnit.Framework; + +namespace CliFx.Tests +{ + [TestFixture] + public class CliApplicationTests + { + [Test] + public async Task RunAsync_Test() + { + // Arrange + var command = new TestCommand(); + var expectedExitCode = await command.ExecuteAsync(); + + var commandResolverMock = new Mock(); + commandResolverMock.Setup(m => m.ResolveCommand(It.IsAny>())).Returns(command); + var commandResolver = commandResolverMock.Object; + + var application = new CliApplication(commandResolver); + + // Act + var exitCodeValue = await application.RunAsync(); + + // Assert + Assert.That(exitCodeValue, Is.EqualTo(expectedExitCode.Value)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj new file mode 100644 index 0000000..4e246c7 --- /dev/null +++ b/CliFx.Tests/CliFx.Tests.csproj @@ -0,0 +1,23 @@ + + + + net45 + false + true + latest + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CliFx.Tests/CommandOptionConverterTests.cs b/CliFx.Tests/CommandOptionConverterTests.cs new file mode 100644 index 0000000..0d90c58 --- /dev/null +++ b/CliFx.Tests/CommandOptionConverterTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using CliFx.Services; +using CliFx.Tests.TestObjects; +using NUnit.Framework; + +namespace CliFx.Tests +{ + [TestFixture] + public class CommandOptionConverterTests + { + private static IEnumerable GetData_ConvertOption() + { + yield return new TestCaseData("value", typeof(string), "value") + .SetName("To string"); + + yield return new TestCaseData("value", typeof(object), "value") + .SetName("To object"); + + yield return new TestCaseData("true", typeof(bool), true) + .SetName("To bool (true)"); + + yield return new TestCaseData("false", typeof(bool), false) + .SetName("To bool (false)"); + + yield return new TestCaseData(null, typeof(bool), true) + .SetName("To bool (switch)"); + + yield return new TestCaseData("123", typeof(int), 123) + .SetName("To int"); + + yield return new TestCaseData("123.45", typeof(double), 123.45) + .SetName("To double"); + + yield return new TestCaseData("28 Apr 1995", typeof(DateTime), new DateTime(1995, 04, 28)) + .SetName("To DateTime"); + + yield return new TestCaseData("28 Apr 1995", typeof(DateTimeOffset), new DateTimeOffset(new DateTime(1995, 04, 28))) + .SetName("To DateTimeOffset"); + + yield return new TestCaseData("00:14:59", typeof(TimeSpan), new TimeSpan(00, 14, 59)) + .SetName("To TimeSpan"); + + yield return new TestCaseData("value2", typeof(TestEnum), TestEnum.Value2) + .SetName("To enum"); + + yield return new TestCaseData("666", typeof(int?), 666) + .SetName("To int? (with value)"); + + yield return new TestCaseData(null, typeof(int?), null) + .SetName("To int? (no value)"); + + yield return new TestCaseData("value3", typeof(TestEnum?), TestEnum.Value3) + .SetName("To enum? (with value)"); + + yield return new TestCaseData(null, typeof(TestEnum?), null) + .SetName("To enum? (no value)"); + + yield return new TestCaseData("01:00:00", typeof(TimeSpan?), new TimeSpan(01, 00, 00)) + .SetName("To TimeSpan? (with value)"); + + yield return new TestCaseData(null, typeof(TimeSpan?), null) + .SetName("To TimeSpan? (no value)"); + } + + [Test] + [TestCaseSource(nameof(GetData_ConvertOption))] + public void ConvertOption_Test(string value, Type targetType, object expectedConvertedValue) + { + // Arrange + var converter = new CommandOptionConverter(); + + // Act + var convertedValue = converter.ConvertOption(value, targetType); + + // Assert + Assert.That(convertedValue, Is.EqualTo(expectedConvertedValue)); + + if (convertedValue != null) + Assert.That(convertedValue, Is.AssignableTo(targetType)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/CommandOptionParserTests.cs b/CliFx.Tests/CommandOptionParserTests.cs new file mode 100644 index 0000000..cc88b11 --- /dev/null +++ b/CliFx.Tests/CommandOptionParserTests.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using CliFx.Models; +using CliFx.Services; +using NUnit.Framework; + +namespace CliFx.Tests +{ + [TestFixture] + public class CommandOptionParserTests + { + private static IEnumerable GetData_ParseOptions() + { + yield return new TestCaseData( + new string[0], + CommandOptionSet.Empty + ).SetName("No arguments"); + + yield return new TestCaseData( + new[] {"--argument", "value"}, + new CommandOptionSet(new Dictionary + { + {"argument", "value"} + }) + ).SetName("Single argument"); + + yield return new TestCaseData( + new[] {"--argument1", "value1", "--argument2", "value2", "--argument3", "value3"}, + new CommandOptionSet(new Dictionary + { + {"argument1", "value1"}, + {"argument2", "value2"}, + {"argument3", "value3"} + }) + ).SetName("Multiple arguments"); + + yield return new TestCaseData( + new[] {"-a", "value"}, + new CommandOptionSet(new Dictionary + { + {"a", "value"} + }) + ).SetName("Single short argument"); + + yield return new TestCaseData( + new[] {"-a", "value1", "-b", "value2", "-c", "value3"}, + new CommandOptionSet(new Dictionary + { + {"a", "value1"}, + {"b", "value2"}, + {"c", "value3"} + }) + ).SetName("Multiple short arguments"); + + yield return new TestCaseData( + new[] {"--argument1", "value1", "-b", "value2", "--argument3", "value3"}, + new CommandOptionSet(new Dictionary + { + {"argument1", "value1"}, + {"b", "value2"}, + {"argument3", "value3"} + }) + ).SetName("Multiple mixed arguments"); + + yield return new TestCaseData( + new[] {"--switch"}, + new CommandOptionSet(new Dictionary + { + {"switch", null} + }) + ).SetName("Single switch"); + + yield return new TestCaseData( + new[] {"--switch1", "--switch2", "--switch3"}, + new CommandOptionSet(new Dictionary + { + {"switch1", null}, + {"switch2", null}, + {"switch3", null} + }) + ).SetName("Multiple switches"); + + yield return new TestCaseData( + new[] {"-s"}, + new CommandOptionSet(new Dictionary + { + {"s", null} + }) + ).SetName("Single short switch"); + + yield return new TestCaseData( + new[] {"-a", "-b", "-c"}, + new CommandOptionSet(new Dictionary + { + {"a", null}, + {"b", null}, + {"c", null} + }) + ).SetName("Multiple short switches"); + + yield return new TestCaseData( + new[] {"-abc"}, + new CommandOptionSet(new Dictionary + { + {"a", null}, + {"b", null}, + {"c", null} + }) + ).SetName("Multiple stacked short switches"); + + yield return new TestCaseData( + new[] {"command"}, + new CommandOptionSet("command") + ).SetName("No arguments (with command name)"); + + yield return new TestCaseData( + new[] {"command", "--argument", "value"}, + new CommandOptionSet("command", new Dictionary + { + {"argument", "value"} + }) + ).SetName("Single argument (with command name)"); + } + + [Test] + [TestCaseSource(nameof(GetData_ParseOptions))] + public void ParseOptions_Test(IReadOnlyList commandLineArguments, CommandOptionSet expectedCommandOptionSet) + { + // Arrange + var parser = new CommandOptionParser(); + + // Act + var optionSet = parser.ParseOptions(commandLineArguments); + + // Assert + Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName)); + Assert.That(optionSet.Options, Is.EqualTo(expectedCommandOptionSet.Options)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/CommandResolverTests.cs b/CliFx.Tests/CommandResolverTests.cs new file mode 100644 index 0000000..8ec5f2c --- /dev/null +++ b/CliFx.Tests/CommandResolverTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using CliFx.Exceptions; +using CliFx.Models; +using CliFx.Services; +using CliFx.Tests.TestObjects; +using Moq; +using NUnit.Framework; + +namespace CliFx.Tests +{ + [TestFixture] + public class CommandResolverTests + { + private static IEnumerable GetData_ResolveCommand() + { + yield return new TestCaseData( + new CommandOptionSet(new Dictionary + { + {"int", "13"} + }), + new TestCommand {IntOption = 13} + ).SetName("Single option"); + + yield return new TestCaseData( + new CommandOptionSet(new Dictionary + { + {"int", "13"}, + {"str", "hello world" } + }), + new TestCommand { IntOption = 13, StringOption = "hello world"} + ).SetName("Multiple options"); + + yield return new TestCaseData( + new CommandOptionSet(new Dictionary + { + {"i", "13"} + }), + new TestCommand { IntOption = 13 } + ).SetName("Single short option"); + + yield return new TestCaseData( + new CommandOptionSet("command", new Dictionary + { + {"int", "13"} + }), + new TestCommand { IntOption = 13 } + ).SetName("Single option (with command name)"); + } + + [Test] + [TestCaseSource(nameof(GetData_ResolveCommand))] + public void ResolveCommand_Test(CommandOptionSet commandOptionSet, TestCommand expectedCommand) + { + // Arrange + var commandTypes = new[] {typeof(TestCommand)}; + + var typeProviderMock = new Mock(); + typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes); + var typeProvider = typeProviderMock.Object; + + var optionParserMock = new Mock(); + optionParserMock.Setup(m => m.ParseOptions(It.IsAny>())).Returns(commandOptionSet); + var optionParser = optionParserMock.Object; + + var optionConverter = new CommandOptionConverter(); + + var resolver = new CommandResolver(typeProvider, optionParser, optionConverter); + + // Act + var command = resolver.ResolveCommand() as TestCommand; + + // Assert + Assert.That(command, Is.Not.Null); + Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption)); + Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption)); + } + + private static IEnumerable GetData_ResolveCommand_IsRequired() + { + yield return new TestCaseData( + CommandOptionSet.Empty + ).SetName("No options"); + + yield return new TestCaseData( + new CommandOptionSet(new Dictionary + { + {"str", "hello world"} + }) + ).SetName("Required option is not set"); + } + + [Test] + [TestCaseSource(nameof(GetData_ResolveCommand_IsRequired))] + public void ResolveCommand_IsRequired_Test(CommandOptionSet commandOptionSet) + { + // Arrange + var commandTypes = new[] { typeof(TestCommand) }; + + var typeProviderMock = new Mock(); + typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes); + var typeProvider = typeProviderMock.Object; + + var optionParserMock = new Mock(); + optionParserMock.Setup(m => m.ParseOptions(It.IsAny>())).Returns(commandOptionSet); + var optionParser = optionParserMock.Object; + + var optionConverter = new CommandOptionConverter(); + + var resolver = new CommandResolver(typeProvider, optionParser, optionConverter); + + // Act & Assert + Assert.Throws(() => resolver.ResolveCommand()); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/DummyTests.cs b/CliFx.Tests/DummyTests.cs new file mode 100644 index 0000000..0020d01 --- /dev/null +++ b/CliFx.Tests/DummyTests.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Threading.Tasks; +using CliWrap; +using NUnit.Framework; + +namespace CliFx.Tests +{ + [TestFixture] + public class DummyTests + { + private string DummyFilePath => Path.Combine(TestContext.CurrentContext.TestDirectory, "CliFx.Tests.Dummy.exe"); + + [Test] + [TestCase("", "Hello world")] + [TestCase("-t .NET", "Hello .NET")] + [TestCase("-e", "Hello world!!!")] + [TestCase("add --a 1 --b 2", "3")] + [TestCase("add --a 2.75 --b 3.6", "6.35")] + [TestCase("log --value 100", "2")] + [TestCase("log --value 256 --base 2", "8")] + public async Task Execute_Test(string arguments, string expectedOutput) + { + // Act + var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); + + // Assert + Assert.That(result.ExitCode, Is.Zero); + Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput)); + Assert.That(result.StandardError.Trim(), Is.Empty); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/TestObjects/TestCommand.cs b/CliFx.Tests/TestObjects/TestCommand.cs new file mode 100644 index 0000000..f2d7188 --- /dev/null +++ b/CliFx.Tests/TestObjects/TestCommand.cs @@ -0,0 +1,18 @@ +using CliFx.Attributes; +using CliFx.Models; + +namespace CliFx.Tests.TestObjects +{ + [DefaultCommand] + [Command("command")] + public class TestCommand : Command + { + [CommandOption("int", ShortName = 'i', IsRequired = true)] + public int IntOption { get; set; } = 24; + + [CommandOption("str", ShortName = 's')] + public string StringOption { get; set; } = "foo bar"; + + public override ExitCode Execute() => new ExitCode(IntOption, StringOption); + } +} \ No newline at end of file diff --git a/CliFx.Tests/TestObjects/TestEnum.cs b/CliFx.Tests/TestObjects/TestEnum.cs new file mode 100644 index 0000000..6035f37 --- /dev/null +++ b/CliFx.Tests/TestObjects/TestEnum.cs @@ -0,0 +1,9 @@ +namespace CliFx.Tests.TestObjects +{ + public enum TestEnum + { + Value1, + Value2, + Value3 + } +} \ No newline at end of file diff --git a/CliFx.sln b/CliFx.sln new file mode 100644 index 0000000..6410a54 --- /dev/null +++ b/CliFx.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.352 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx", "CliFx\CliFx.csproj", "{C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{4904B3EB-3286-4F1B-8B74-6FF051C8E787}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x64.Build.0 = Debug|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Debug|x86.Build.0 = Debug|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|Any CPU.Build.0 = Release|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x64.ActiveCfg = Release|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x64.Build.0 = Release|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x86.ActiveCfg = Release|Any CPU + {C0D60B0B-63CF-4DCA-916F-BDB40CEDCE35}.Release|x86.Build.0 = Release|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x64.Build.0 = Debug|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Debug|x86.Build.0 = Debug|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|Any CPU.Build.0 = Release|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.ActiveCfg = Release|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.Build.0 = Release|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.ActiveCfg = Release|Any CPU + {268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.Build.0 = Release|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.ActiveCfg = Debug|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.Build.0 = Debug|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.ActiveCfg = Debug|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.Build.0 = Debug|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.Build.0 = Release|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.ActiveCfg = Release|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.Build.0 = Release|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.ActiveCfg = Release|Any CPU + {4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6ACC950B-5F93-429C-A204-6315A92AD3A1} + EndGlobalSection +EndGlobal diff --git a/CliFx/Attributes/CommandAttribute.cs b/CliFx/Attributes/CommandAttribute.cs new file mode 100644 index 0000000..dbccc97 --- /dev/null +++ b/CliFx/Attributes/CommandAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace CliFx.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class CommandAttribute : Attribute + { + public string Name { get; } + + public CommandAttribute(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs new file mode 100644 index 0000000..67f49d0 --- /dev/null +++ b/CliFx/Attributes/CommandOptionAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace CliFx.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class CommandOptionAttribute : Attribute + { + public string Name { get; } + + public char ShortName { get; set; } + + public bool IsRequired { get; set; } + + public string Description { get; set; } + + public CommandOptionAttribute(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/CliFx/Attributes/DefaultCommandAttribute.cs b/CliFx/Attributes/DefaultCommandAttribute.cs new file mode 100644 index 0000000..43195cd --- /dev/null +++ b/CliFx/Attributes/DefaultCommandAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace CliFx.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class DefaultCommandAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs new file mode 100644 index 0000000..75414d2 --- /dev/null +++ b/CliFx/CliApplication.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using CliFx.Services; + +namespace CliFx +{ + public partial class CliApplication : ICliApplication + { + private readonly ICommandResolver _commandResolver; + + public CliApplication(ICommandResolver commandResolver) + { + _commandResolver = commandResolver; + } + + public CliApplication() + : this(GetDefaultCommandResolver(Assembly.GetCallingAssembly())) + { + } + + public async Task RunAsync(IReadOnlyList commandLineArguments) + { + // Resolve and execute command + var command = _commandResolver.ResolveCommand(commandLineArguments); + var exitCode = await command.ExecuteAsync(); + + // TODO: print message if error? + + return exitCode.Value; + } + } + + public partial class CliApplication + { + private static ICommandResolver GetDefaultCommandResolver(Assembly assembly) + { + var typeProvider = TypeProvider.FromAssembly(assembly); + var commandOptionParser = new CommandOptionParser(); + var commandOptionConverter = new CommandOptionConverter(); + + return new CommandResolver(typeProvider, commandOptionParser, commandOptionConverter); + } + } +} \ No newline at end of file diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj new file mode 100644 index 0000000..028d937 --- /dev/null +++ b/CliFx/CliFx.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.0;net45 + latest + + + \ No newline at end of file diff --git a/CliFx/Command.cs b/CliFx/Command.cs new file mode 100644 index 0000000..7442e7f --- /dev/null +++ b/CliFx/Command.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using CliFx.Models; + +namespace CliFx +{ + public abstract class Command + { + public virtual ExitCode Execute() => throw new InvalidOperationException( + "Can't execute command because its execution method is not defined. " + + $"Override Execute or ExecuteAsync on {GetType().Name} in order to make it executable."); + + public virtual Task ExecuteAsync() => Task.FromResult(Execute()); + } +} \ No newline at end of file diff --git a/CliFx/Exceptions/CommandResolveException.cs b/CliFx/Exceptions/CommandResolveException.cs new file mode 100644 index 0000000..86684f1 --- /dev/null +++ b/CliFx/Exceptions/CommandResolveException.cs @@ -0,0 +1,21 @@ +using System; + +namespace CliFx.Exceptions +{ + public class CommandResolveException : Exception + { + public CommandResolveException() + { + } + + public CommandResolveException(string message) + : base(message) + { + } + + public CommandResolveException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/CliFx/Extensions.cs b/CliFx/Extensions.cs new file mode 100644 index 0000000..972387e --- /dev/null +++ b/CliFx/Extensions.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CliFx +{ + public static class Extensions + { + public static Task RunAsync(this ICliApplication application) => + application.RunAsync(Environment.GetCommandLineArgs().Skip(1).ToArray()); + } +} \ No newline at end of file diff --git a/CliFx/ICliApplication.cs b/CliFx/ICliApplication.cs new file mode 100644 index 0000000..d5f4b33 --- /dev/null +++ b/CliFx/ICliApplication.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace CliFx +{ + public interface ICliApplication + { + Task RunAsync(IReadOnlyList commandLineArguments); + } +} \ No newline at end of file diff --git a/CliFx/Internal/CommandOptionProperty.cs b/CliFx/Internal/CommandOptionProperty.cs new file mode 100644 index 0000000..3b1dd15 --- /dev/null +++ b/CliFx/Internal/CommandOptionProperty.cs @@ -0,0 +1,48 @@ +using System; +using System.Reflection; +using CliFx.Attributes; + +namespace CliFx.Internal +{ + internal partial class CommandOptionProperty + { + private readonly PropertyInfo _property; + + public Type Type => _property.PropertyType; + + public string Name { get; } + + public char ShortName { get; } + + public bool IsRequired { get; } + + public string Description { get; } + + public CommandOptionProperty(PropertyInfo property, string name, char shortName, bool isRequired, string description) + { + _property = property; + Name = name; + ShortName = shortName; + IsRequired = isRequired; + Description = description; + } + + public void SetValue(Command command, object value) => _property.SetValue(command, value); + } + + internal partial class CommandOptionProperty + { + public static bool IsValid(PropertyInfo property) => property.IsDefined(typeof(CommandOptionAttribute)); + + public static CommandOptionProperty Initialize(PropertyInfo property) + { + if (!IsValid(property)) + throw new InvalidOperationException($"[{property.Name}] is not a valid command option property."); + + var attribute = property.GetCustomAttribute(); + + return new CommandOptionProperty(property, attribute.Name, attribute.ShortName, attribute.IsRequired, + attribute.Description); + } + } +} \ No newline at end of file diff --git a/CliFx/Internal/CommandType.cs b/CliFx/Internal/CommandType.cs new file mode 100644 index 0000000..520fcfa --- /dev/null +++ b/CliFx/Internal/CommandType.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using CliFx.Attributes; + +namespace CliFx.Internal +{ + internal partial class CommandType + { + private readonly Type _type; + + public string Name { get; } + + public bool IsDefault { get; } + + public CommandType(Type type, string name, bool isDefault) + { + _type = type; + Name = name; + IsDefault = isDefault; + } + + public IEnumerable GetOptionProperties() => _type.GetProperties() + .Where(CommandOptionProperty.IsValid) + .Select(CommandOptionProperty.Initialize); + + public Command Activate() => (Command) Activator.CreateInstance(_type); + } + + internal partial class CommandType + { + public static bool IsValid(Type type) => + // Derives from Command + type.IsDerivedFrom(typeof(Command)) && + // Marked with DefaultCommandAttribute or CommandAttribute + (type.IsDefined(typeof(DefaultCommandAttribute)) || type.IsDefined(typeof(CommandAttribute))); + + public static CommandType Initialize(Type type) + { + if (!IsValid(type)) + throw new InvalidOperationException($"[{type.Name}] is not a valid command type."); + + var name = type.GetCustomAttribute()?.Name; + var isDefault = type.IsDefined(typeof(DefaultCommandAttribute)); + + return new CommandType(type, name, isDefault); + } + + public static IEnumerable GetCommandTypes(IEnumerable types) => types.Where(IsValid).Select(Initialize); + } +} \ No newline at end of file diff --git a/CliFx/Internal/Extensions.cs b/CliFx/Internal/Extensions.cs new file mode 100644 index 0000000..ca9bc2a --- /dev/null +++ b/CliFx/Internal/Extensions.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace CliFx.Internal +{ + internal static class Extensions + { + public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s); + + public static string SubstringUntil(this string s, string sub, StringComparison comparison = StringComparison.Ordinal) + { + var index = s.IndexOf(sub, comparison); + return index < 0 ? s : s.Substring(0, index); + } + + public static string SubstringAfter(this string s, string sub, StringComparison comparison = StringComparison.Ordinal) + { + var index = s.IndexOf(sub, comparison); + return index < 0 ? string.Empty : s.Substring(index + sub.Length, s.Length - index - sub.Length); + } + + public static TValue GetValueOrDefault(this IReadOnlyDictionary dic, TKey key) => + dic.TryGetValue(key, out var result) ? result : default; + + public static string TrimStart(this string s, string sub, StringComparison comparison = StringComparison.Ordinal) + { + while (s.StartsWith(sub, comparison)) + s = s.Substring(sub.Length); + + return s; + } + + public static string TrimEnd(this string s, string sub, StringComparison comparison = StringComparison.Ordinal) + { + while (s.EndsWith(sub, comparison)) + s = s.Substring(0, s.Length - sub.Length); + + return s; + } + + public static string JoinToString(this IEnumerable source, string separator) => string.Join(separator, source); + + public static bool IsDerivedFrom(this Type type, Type baseType) + { + var currentType = type; + while (currentType != null) + { + if (currentType == baseType) + return true; + + currentType = currentType.BaseType; + } + + return false; + } + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandOptionSet.cs b/CliFx/Models/CommandOptionSet.cs new file mode 100644 index 0000000..fdd40ce --- /dev/null +++ b/CliFx/Models/CommandOptionSet.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using CliFx.Internal; + +namespace CliFx.Models +{ + public partial class CommandOptionSet + { + public string CommandName { get; } + + public IReadOnlyDictionary Options { get; } + + public CommandOptionSet(string commandName, IReadOnlyDictionary options) + { + CommandName = commandName; + Options = options; + } + + public CommandOptionSet(IReadOnlyDictionary options) + : this(null, options) + { + } + + public CommandOptionSet(string commandName) + : this(commandName, new Dictionary()) + { + } + + public override string ToString() => !CommandName.IsNullOrWhiteSpace() + ? $"{CommandName} / {Options.Count} option(s)" + : $"{Options.Count} option(s)"; + } + + public partial class CommandOptionSet + { + public static CommandOptionSet Empty { get; } = new CommandOptionSet(new Dictionary()); + } +} \ No newline at end of file diff --git a/CliFx/Models/ExitCode.cs b/CliFx/Models/ExitCode.cs new file mode 100644 index 0000000..d730edb --- /dev/null +++ b/CliFx/Models/ExitCode.cs @@ -0,0 +1,26 @@ +using System.Globalization; + +namespace CliFx.Models +{ + public partial class ExitCode + { + public int Value { get; } + + public string Message { get; } + + public bool IsSuccess => Value == 0; + + public ExitCode(int value, string message = null) + { + Value = value; + Message = message; + } + + public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); + } + + public partial class ExitCode + { + public static ExitCode Success { get; } = new ExitCode(0); + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandOptionConverter.cs b/CliFx/Services/CommandOptionConverter.cs new file mode 100644 index 0000000..7c9a526 --- /dev/null +++ b/CliFx/Services/CommandOptionConverter.cs @@ -0,0 +1,56 @@ +using System; +using System.Globalization; +using CliFx.Internal; + +namespace CliFx.Services +{ + public class CommandOptionConverter : ICommandOptionConverter + { + private readonly IFormatProvider _formatProvider; + + public CommandOptionConverter(IFormatProvider formatProvider) + { + _formatProvider = formatProvider; + } + + public CommandOptionConverter() + : this(CultureInfo.InvariantCulture) + { + } + + public object ConvertOption(string value, Type targetType) + { + // String or object + if (targetType == typeof(string) || targetType == typeof(object)) + return value; + + // Bool + if (targetType == typeof(bool)) + return value.IsNullOrWhiteSpace() || bool.Parse(value); + + // DateTime + if (targetType == typeof(DateTime)) + return DateTime.Parse(value, _formatProvider); + + // DateTimeOffset + if (targetType == typeof(DateTimeOffset)) + return DateTimeOffset.Parse(value, _formatProvider); + + // TimeSpan + if (targetType == typeof(TimeSpan)) + return TimeSpan.Parse(value, _formatProvider); + + // Enum + if (targetType.IsEnum) + return Enum.Parse(targetType, value, true); + + // Nullable + var nullableUnderlyingType = Nullable.GetUnderlyingType(targetType); + if (nullableUnderlyingType != null) + return !value.IsNullOrWhiteSpace() ? ConvertOption(value, nullableUnderlyingType) : null; + + // All other types + return Convert.ChangeType(value, targetType, _formatProvider); + } + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandOptionParser.cs b/CliFx/Services/CommandOptionParser.cs new file mode 100644 index 0000000..ada4964 --- /dev/null +++ b/CliFx/Services/CommandOptionParser.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using CliFx.Internal; +using CliFx.Models; + +namespace CliFx.Services +{ + public class CommandOptionParser : ICommandOptionParser + { + public CommandOptionSet ParseOptions(IReadOnlyList commandLineArguments) + { + // Initialize command name placeholder + string commandName = null; + + // Initialize options + var options = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Keep track of the last option's name + string optionName = null; + + // Loop through all arguments + var isFirstArgument = true; + foreach (var commandLineArgument in commandLineArguments) + { + // Option name + if (commandLineArgument.StartsWith("--", StringComparison.OrdinalIgnoreCase)) + { + // Extract option name (skip 2 chars) + optionName = commandLineArgument.Substring(2); + options[optionName] = null; + } + + // Short option name + else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase) && commandLineArgument.Length == 2) + { + // Extract option name (skip 1 char) + optionName = commandLineArgument.Substring(1); + options[optionName] = null; + } + + // Multiple stacked short options + else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase)) + { + optionName = null; + foreach (var c in commandLineArgument.Substring(1)) + { + options[c.ToString(CultureInfo.InvariantCulture)] = null; + } + } + + // Command name + else if (isFirstArgument) + { + commandName = commandLineArgument; + } + + // Option value + else if (!optionName.IsNullOrWhiteSpace()) + { + // ReSharper disable once AssignNullToNotNullAttribute + options[optionName] = commandLineArgument; + } + + isFirstArgument = false; + } + + return new CommandOptionSet(commandName, options); + } + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandResolver.cs b/CliFx/Services/CommandResolver.cs new file mode 100644 index 0000000..81305da --- /dev/null +++ b/CliFx/Services/CommandResolver.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using CliFx.Attributes; +using CliFx.Exceptions; +using CliFx.Internal; + +namespace CliFx.Services +{ + public class CommandResolver : ICommandResolver + { + private readonly ITypeProvider _typeProvider; + private readonly ICommandOptionParser _commandOptionParser; + private readonly ICommandOptionConverter _commandOptionConverter; + + public CommandResolver(ITypeProvider typeProvider, + ICommandOptionParser commandOptionParser, ICommandOptionConverter commandOptionConverter) + { + _typeProvider = typeProvider; + _commandOptionParser = commandOptionParser; + _commandOptionConverter = commandOptionConverter; + } + + private IEnumerable GetCommandTypes() => CommandType.GetCommandTypes(_typeProvider.GetTypes()); + + private CommandType GetDefaultCommandType() + { + // Get command types marked as default + var defaultCommandTypes = GetCommandTypes().Where(t => t.IsDefault).ToArray(); + + // If there's only one type - return + if (defaultCommandTypes.Length == 1) + return defaultCommandTypes.Single(); + + // If there are multiple - throw + if (defaultCommandTypes.Length > 1) + { + throw new CommandResolveException( + "Can't resolve default command because there is more than one command marked as default. " + + $"Make sure you apply {nameof(DefaultCommandAttribute)} only to one command."); + } + + // If there aren't any - throw + throw new CommandResolveException( + "Can't resolve default command because there are no commands marked as default. " + + $"Apply {nameof(DefaultCommandAttribute)} to the default command."); + } + + private CommandType GetCommandType(string name) + { + // Get command types with given name + var matchingCommandTypes = + GetCommandTypes().Where(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)).ToArray(); + + // If there's only one type - return + if (matchingCommandTypes.Length == 1) + return matchingCommandTypes.Single(); + + // If there are multiple - throw + if (matchingCommandTypes.Length > 1) + { + throw new CommandResolveException( + $"Can't resolve command because there is more than one command named [{name}]. " + + "Make sure all command names are unique and keep in mind that comparison is case-insensitive."); + } + + // If there aren't any - throw + throw new CommandResolveException( + $"Can't resolve command because none of the commands is named [{name}]. " + + $"Apply {nameof(CommandAttribute)} to give command a name."); + } + + public Command ResolveCommand(IReadOnlyList commandLineArguments) + { + var optionSet = _commandOptionParser.ParseOptions(commandLineArguments); + + // Get command type + var commandType = !optionSet.CommandName.IsNullOrWhiteSpace() + ? GetCommandType(optionSet.CommandName) + : GetDefaultCommandType(); + + // Activate command + var command = commandType.Activate(); + + // Set command options + foreach (var property in commandType.GetOptionProperties()) + { + // If option set contains this property - set value + if (optionSet.Options.TryGetValue(property.Name, out var value) || + optionSet.Options.TryGetValue(property.ShortName.ToString(CultureInfo.InvariantCulture), out value)) + { + var convertedValue = _commandOptionConverter.ConvertOption(value, property.Type); + property.SetValue(command, convertedValue); + } + // If the property is missing but it's required - throw + else if (property.IsRequired) + { + throw new CommandResolveException( + $"Can't resolve command [{optionSet.CommandName}] because required property [{property.Name}] is not set."); + } + } + + return command; + } + } +} \ No newline at end of file diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs new file mode 100644 index 0000000..c391a19 --- /dev/null +++ b/CliFx/Services/Extensions.cs @@ -0,0 +1,7 @@ +namespace CliFx.Services +{ + public static class Extensions + { + public static Command ResolveCommand(this ICommandResolver commandResolver) => commandResolver.ResolveCommand(new string[0]); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandOptionConverter.cs b/CliFx/Services/ICommandOptionConverter.cs new file mode 100644 index 0000000..395bf30 --- /dev/null +++ b/CliFx/Services/ICommandOptionConverter.cs @@ -0,0 +1,9 @@ +using System; + +namespace CliFx.Services +{ + public interface ICommandOptionConverter + { + object ConvertOption(string value, Type targetType); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandOptionParser.cs b/CliFx/Services/ICommandOptionParser.cs new file mode 100644 index 0000000..7cd4c1f --- /dev/null +++ b/CliFx/Services/ICommandOptionParser.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using CliFx.Models; + +namespace CliFx.Services +{ + public interface ICommandOptionParser + { + CommandOptionSet ParseOptions(IReadOnlyList commandLineArguments); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandResolver.cs b/CliFx/Services/ICommandResolver.cs new file mode 100644 index 0000000..76e8407 --- /dev/null +++ b/CliFx/Services/ICommandResolver.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace CliFx.Services +{ + public interface ICommandResolver + { + Command ResolveCommand(IReadOnlyList commandLineArguments); + } +} \ No newline at end of file diff --git a/CliFx/Services/ITypeProvider.cs b/CliFx/Services/ITypeProvider.cs new file mode 100644 index 0000000..bc7c5da --- /dev/null +++ b/CliFx/Services/ITypeProvider.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace CliFx.Services +{ + public interface ITypeProvider + { + IReadOnlyList GetTypes(); + } +} \ No newline at end of file diff --git a/CliFx/Services/TypeProvider.cs b/CliFx/Services/TypeProvider.cs new file mode 100644 index 0000000..93c92f7 --- /dev/null +++ b/CliFx/Services/TypeProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace CliFx.Services +{ + public partial class TypeProvider : ITypeProvider + { + private readonly IReadOnlyList _types; + + public TypeProvider(IReadOnlyList types) + { + _types = types; + } + + public TypeProvider(params Type[] types) + : this((IReadOnlyList) types) + { + } + + public IReadOnlyList GetTypes() => _types; + } + + public partial class TypeProvider + { + public static TypeProvider FromAssembly(Assembly assembly) => new TypeProvider(assembly.GetExportedTypes()); + + public static TypeProvider FromAssemblies(IReadOnlyList assemblies) => + new TypeProvider(assemblies.SelectMany(a => a.ExportedTypes).ToArray()); + + public static TypeProvider FromAssemblies(params Assembly[] assemblies) => FromAssemblies((IReadOnlyList) assemblies); + } +} \ No newline at end of file