From 2bdb2bddc8b7ec453ecc310a5a5b112987f58c3b Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Sat, 20 Jul 2019 22:50:57 +0300 Subject: [PATCH] Rework architecture and implement auto help --- CliFx.Tests.Dummy/Commands/AddCommand.cs | 8 +- CliFx.Tests.Dummy/Commands/DefaultCommand.cs | 10 +- CliFx.Tests.Dummy/Commands/LogCommand.cs | 4 +- CliFx.Tests/CliApplicationTests.cs | 16 +- CliFx.Tests/CommandInitializerTests.cs | 140 ++++++++++++++ CliFx.Tests/CommandInputParserTests.cs | 170 ++++++++++++++++ ...cs => CommandOptionInputConverterTests.cs} | 62 +++--- CliFx.Tests/CommandOptionParserTests.cs | 183 ------------------ CliFx.Tests/CommandResolverTests.cs | 97 ---------- CliFx.Tests/CommandSchemaResolverTests.cs | 69 +++++++ CliFx.Tests/DummyTests.cs | 50 ++++- CliFx/Attributes/CommandAttribute.cs | 8 + CliFx/Attributes/CommandOptionAttribute.cs | 2 + CliFx/Attributes/DefaultCommandAttribute.cs | 13 -- CliFx/CliApplication.cs | 16 +- CliFx/Command.cs | 52 ++++- CliFx/ICommand.cs | 2 + CliFx/Internal/CommandOptionProperty.cs | 48 ----- CliFx/Internal/CommandType.cs | 60 ------ CliFx/Internal/HashCodeBuilder.cs | 38 ++++ CliFx/Models/CommandContext.cs | 17 ++ CliFx/Models/CommandInput.cs | 67 +++++++ CliFx/Models/CommandInputEqualityComparer.cs | 34 ++++ ...CommandOption.cs => CommandOptionInput.cs} | 8 +- .../CommandOptionInputEqualityComparer.cs | 34 ++++ CliFx/Models/CommandOptionSchema.cs | 32 +++ .../CommandOptionSchemaEqualityComparer.cs | 39 ++++ CliFx/Models/CommandOptionSet.cs | 47 ----- CliFx/Models/CommandSchema.cs | 27 +++ CliFx/Models/CommandSchemaEqualityComparer.cs | 40 ++++ CliFx/Models/Extensions.cs | 2 +- CliFx/Models/TextSpan.cs | 24 +++ CliFx/Services/CommandInitializer.cs | 137 +++++++++++++ ...dOptionParser.cs => CommandInputParser.cs} | 7 +- ...rter.cs => CommandOptionInputConverter.cs} | 9 +- CliFx/Services/CommandResolver.cs | 114 ----------- CliFx/Services/CommandSchemaResolver.cs | 74 +++++++ CliFx/Services/ConsoleWriter.cs | 32 +++ CliFx/Services/Extensions.cs | 11 ++ CliFx/Services/HelpTextBuilder.cs | 106 ++++++++++ CliFx/Services/ICommandInitializer.cs | 9 + CliFx/Services/ICommandInputParser.cs | 10 + CliFx/Services/ICommandOptionConverter.cs | 10 - .../Services/ICommandOptionInputConverter.cs | 10 + CliFx/Services/ICommandOptionParser.cs | 10 - CliFx/Services/ICommandResolver.cs | 9 - CliFx/Services/ICommandSchemaResolver.cs | 10 + CliFx/Services/IConsoleWriter.cs | 11 ++ CliFx/Services/IHelpTextBuilder.cs | 9 + CliFx/Services/ITypeActivator.cs | 9 + CliFx/Services/TypeActivator.cs | 9 + 51 files changed, 1348 insertions(+), 667 deletions(-) create mode 100644 CliFx.Tests/CommandInitializerTests.cs create mode 100644 CliFx.Tests/CommandInputParserTests.cs rename CliFx.Tests/{CommandOptionConverterTests.cs => CommandOptionInputConverterTests.cs} (70%) delete mode 100644 CliFx.Tests/CommandOptionParserTests.cs delete mode 100644 CliFx.Tests/CommandResolverTests.cs create mode 100644 CliFx.Tests/CommandSchemaResolverTests.cs delete mode 100644 CliFx/Attributes/DefaultCommandAttribute.cs delete mode 100644 CliFx/Internal/CommandOptionProperty.cs delete mode 100644 CliFx/Internal/CommandType.cs create mode 100644 CliFx/Internal/HashCodeBuilder.cs create mode 100644 CliFx/Models/CommandContext.cs create mode 100644 CliFx/Models/CommandInput.cs create mode 100644 CliFx/Models/CommandInputEqualityComparer.cs rename CliFx/Models/{CommandOption.cs => CommandOptionInput.cs} (62%) create mode 100644 CliFx/Models/CommandOptionInputEqualityComparer.cs create mode 100644 CliFx/Models/CommandOptionSchema.cs create mode 100644 CliFx/Models/CommandOptionSchemaEqualityComparer.cs delete mode 100644 CliFx/Models/CommandOptionSet.cs create mode 100644 CliFx/Models/CommandSchema.cs create mode 100644 CliFx/Models/CommandSchemaEqualityComparer.cs create mode 100644 CliFx/Models/TextSpan.cs create mode 100644 CliFx/Services/CommandInitializer.cs rename CliFx/Services/{CommandOptionParser.cs => CommandInputParser.cs} (89%) rename CliFx/Services/{CommandOptionConverter.cs => CommandOptionInputConverter.cs} (96%) delete mode 100644 CliFx/Services/CommandResolver.cs create mode 100644 CliFx/Services/CommandSchemaResolver.cs create mode 100644 CliFx/Services/ConsoleWriter.cs create mode 100644 CliFx/Services/Extensions.cs create mode 100644 CliFx/Services/HelpTextBuilder.cs create mode 100644 CliFx/Services/ICommandInitializer.cs create mode 100644 CliFx/Services/ICommandInputParser.cs delete mode 100644 CliFx/Services/ICommandOptionConverter.cs create mode 100644 CliFx/Services/ICommandOptionInputConverter.cs delete mode 100644 CliFx/Services/ICommandOptionParser.cs delete mode 100644 CliFx/Services/ICommandResolver.cs create mode 100644 CliFx/Services/ICommandSchemaResolver.cs create mode 100644 CliFx/Services/IConsoleWriter.cs create mode 100644 CliFx/Services/IHelpTextBuilder.cs create mode 100644 CliFx/Services/ITypeActivator.cs create mode 100644 CliFx/Services/TypeActivator.cs diff --git a/CliFx.Tests.Dummy/Commands/AddCommand.cs b/CliFx.Tests.Dummy/Commands/AddCommand.cs index dea9b7f..06e6d26 100644 --- a/CliFx.Tests.Dummy/Commands/AddCommand.cs +++ b/CliFx.Tests.Dummy/Commands/AddCommand.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; using CliFx.Attributes; using CliFx.Models; +using CliFx.Services; namespace CliFx.Tests.Dummy.Commands { @@ -13,10 +13,10 @@ namespace CliFx.Tests.Dummy.Commands [CommandOption("values", 'v', IsRequired = true, Description = "Input values.")] public IReadOnlyList Values { get; set; } - public override ExitCode Execute() + protected override ExitCode Process() { var result = Values.Sum(); - Console.WriteLine(result.ToString(CultureInfo.InvariantCulture)); + Output.WriteLine(result.ToString(CultureInfo.InvariantCulture)); return ExitCode.Success; } diff --git a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs index d3fb8e1..5648c02 100644 --- a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs +++ b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs @@ -1,11 +1,11 @@ -using System; -using System.Text; +using System.Text; using CliFx.Attributes; using CliFx.Models; +using CliFx.Services; namespace CliFx.Tests.Dummy.Commands { - [DefaultCommand] + [Command] public class DefaultCommand : Command { [CommandOption("target", 't', Description = "Greeting target.")] @@ -14,7 +14,7 @@ namespace CliFx.Tests.Dummy.Commands [CommandOption('e', Description = "Whether the greeting should be enthusiastic.")] public bool IsEnthusiastic { get; set; } - public override ExitCode Execute() + protected override ExitCode Process() { var buffer = new StringBuilder(); @@ -23,7 +23,7 @@ namespace CliFx.Tests.Dummy.Commands if (IsEnthusiastic) buffer.Append("!!!"); - Console.WriteLine(buffer.ToString()); + Output.WriteLine(buffer.ToString()); return ExitCode.Success; } diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs index 6d178a3..b61e357 100644 --- a/CliFx.Tests.Dummy/Commands/LogCommand.cs +++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs @@ -15,10 +15,10 @@ namespace CliFx.Tests.Dummy.Commands [CommandOption("base", 'b', Description = "Logarithm base.")] public double Base { get; set; } = 10; - public override ExitCode Execute() + protected override ExitCode Process() { var result = Math.Log(Value, Base); - Console.WriteLine(result.ToString(CultureInfo.InvariantCulture)); + Output.WriteLine(result.ToString(CultureInfo.InvariantCulture)); return ExitCode.Success; } diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs index 083e44b..f0d5bee 100644 --- a/CliFx.Tests/CliApplicationTests.cs +++ b/CliFx.Tests/CliApplicationTests.cs @@ -8,10 +8,14 @@ namespace CliFx.Tests { public partial class CliApplicationTests { - [DefaultCommand] - public class TestCommand : Command + [Command] + public class TestCommand : ICommand { - public override ExitCode Execute() => new ExitCode(13); + public static ExitCode ExitCode { get; } = new ExitCode(13); + + public CommandContext Context { get; set; } + + public Task ExecuteAsync() => Task.FromResult(ExitCode); } } @@ -23,14 +27,14 @@ namespace CliFx.Tests { // Arrange var application = new CliApplication( - new CommandOptionParser(), - new CommandResolver(new[] {typeof(TestCommand)}, new CommandOptionConverter())); + new CommandInputParser(), + new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}))); // Act var exitCodeValue = await application.RunAsync(); // Assert - Assert.That(exitCodeValue, Is.EqualTo(13), "Exit code"); + Assert.That(exitCodeValue, Is.EqualTo(TestCommand.ExitCode.Value), "Exit code"); } } } \ No newline at end of file diff --git a/CliFx.Tests/CommandInitializerTests.cs b/CliFx.Tests/CommandInitializerTests.cs new file mode 100644 index 0000000..83213a8 --- /dev/null +++ b/CliFx.Tests/CommandInitializerTests.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Exceptions; +using CliFx.Models; +using CliFx.Services; +using NUnit.Framework; + +namespace CliFx.Tests +{ + public partial class CommandInitializerTests + { + [Command] + public class TestCommand : ICommand + { + [CommandOption("int", 'i', IsRequired = true)] + public int IntOption { get; set; } = 24; + + [CommandOption("str", 's')] + public string StringOption { get; set; } = "foo bar"; + + [CommandOption("bool", 'b', GroupName = "other-group")] + public bool BoolOption { get; set; } + + public CommandContext Context { get; set; } + + public Task ExecuteAsync() => throw new System.NotImplementedException(); + } + } + + [TestFixture] + public partial class CommandInitializerTests + { + private static IEnumerable GetTestCases_InitializeCommand() + { + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("int", "13") + }), + new TestCommand {IntOption = 13} + ); + + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("int", "13"), + new CommandOptionInput("str", "hello world") + }), + new TestCommand {IntOption = 13, StringOption = "hello world"} + ); + + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("i", "13") + }), + new TestCommand {IntOption = 13} + ); + + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("bool") + }), + new TestCommand {BoolOption = true} + ); + + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("b") + }), + new TestCommand {BoolOption = true} + ); + + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("bool"), + new CommandOptionInput("str", "hello world") + }), + new TestCommand {BoolOption = true} + ); + + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("int", "13"), + new CommandOptionInput("str", "hello world"), + new CommandOptionInput("bool") + }), + new TestCommand {IntOption = 13, StringOption = "hello world"} + ); + } + + [Test] + [TestCaseSource(nameof(GetTestCases_InitializeCommand))] + public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand) + { + // Arrange + var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)})); + + // Act + var command = initializer.InitializeCommand(commandInput) as TestCommand; + + // Assert + Assert.Multiple(() => + { + 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)); + Assert.That(command.BoolOption, Is.EqualTo(expectedCommand.BoolOption), nameof(command.BoolOption)); + }); + } + + private static IEnumerable GetTestCases_InitializeCommand_IsRequired() + { + yield return new TestCaseData(CommandInput.Empty); + + yield return new TestCaseData( + new CommandInput(new[] + { + new CommandOptionInput("str", "hello world") + }) + ); + } + + [Test] + [TestCaseSource(nameof(GetTestCases_InitializeCommand_IsRequired))] + public void InitializeCommand_IsRequired_Test(CommandInput commandInput) + { + // Arrange + var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)})); + + // Act & Assert + Assert.Throws(() => initializer.InitializeCommand(commandInput)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/CommandInputParserTests.cs b/CliFx.Tests/CommandInputParserTests.cs new file mode 100644 index 0000000..11596b5 --- /dev/null +++ b/CliFx.Tests/CommandInputParserTests.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using CliFx.Models; +using CliFx.Services; +using NUnit.Framework; + +namespace CliFx.Tests +{ + [TestFixture] + public class CommandInputParserTests + { + private static IEnumerable GetTestCases_ParseInput() + { + yield return new TestCaseData(new string[0], CommandInput.Empty); + + yield return new TestCaseData( + new[] {"--option", "value"}, + new CommandInput(new[] + { + new CommandOptionInput("option", "value") + }) + ); + + yield return new TestCaseData( + new[] {"--option1", "value1", "--option2", "value2"}, + new CommandInput(new[] + { + new CommandOptionInput("option1", "value1"), + new CommandOptionInput("option2", "value2") + }) + ); + + yield return new TestCaseData( + new[] {"--option", "value1", "value2"}, + new CommandInput(new[] + { + new CommandOptionInput("option", new[] {"value1", "value2"}) + }) + ); + + yield return new TestCaseData( + new[] {"--option", "value1", "--option", "value2"}, + new CommandInput(new[] + { + new CommandOptionInput("option", new[] {"value1", "value2"}) + }) + ); + + yield return new TestCaseData( + new[] {"-a", "value"}, + new CommandInput(new[] + { + new CommandOptionInput("a", "value") + }) + ); + + yield return new TestCaseData( + new[] {"-a", "value1", "-b", "value2"}, + new CommandInput(new[] + { + new CommandOptionInput("a", "value1"), + new CommandOptionInput("b", "value2") + }) + ); + + yield return new TestCaseData( + new[] {"-a", "value1", "value2"}, + new CommandInput(new[] + { + new CommandOptionInput("a", new[] {"value1", "value2"}) + }) + ); + + yield return new TestCaseData( + new[] {"-a", "value1", "-a", "value2"}, + new CommandInput(new[] + { + new CommandOptionInput("a", new[] {"value1", "value2"}) + }) + ); + + yield return new TestCaseData( + new[] {"--option1", "value1", "-b", "value2"}, + new CommandInput(new[] + { + new CommandOptionInput("option1", "value1"), + new CommandOptionInput("b", "value2") + }) + ); + + yield return new TestCaseData( + new[] {"--switch"}, + new CommandInput(new[] + { + new CommandOptionInput("switch") + }) + ); + + yield return new TestCaseData( + new[] {"--switch1", "--switch2"}, + new CommandInput(new[] + { + new CommandOptionInput("switch1"), + new CommandOptionInput("switch2") + }) + ); + + yield return new TestCaseData( + new[] {"-s"}, + new CommandInput(new[] + { + new CommandOptionInput("s") + }) + ); + + yield return new TestCaseData( + new[] {"-a", "-b"}, + new CommandInput(new[] + { + new CommandOptionInput("a"), + new CommandOptionInput("b") + }) + ); + + yield return new TestCaseData( + new[] {"-ab"}, + new CommandInput(new[] + { + new CommandOptionInput("a"), + new CommandOptionInput("b") + }) + ); + + yield return new TestCaseData( + new[] {"-ab", "value"}, + new CommandInput(new[] + { + new CommandOptionInput("a"), + new CommandOptionInput("b", "value") + }) + ); + + yield return new TestCaseData( + new[] {"command"}, + new CommandInput("command") + ); + + yield return new TestCaseData( + new[] {"command", "--option", "value"}, + new CommandInput("command", new[] + { + new CommandOptionInput("option", "value") + }) + ); + } + + [Test] + [TestCaseSource(nameof(GetTestCases_ParseInput))] + public void ParseInput_Test(IReadOnlyList commandLineArguments, CommandInput expectedCommandInput) + { + // Arrange + var parser = new CommandInputParser(); + + // Act + var commandInput = parser.ParseInput(commandLineArguments); + + // Assert + Assert.That(commandInput, Is.EqualTo(expectedCommandInput).Using(CommandInputEqualityComparer.Instance)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/CommandOptionConverterTests.cs b/CliFx.Tests/CommandOptionInputConverterTests.cs similarity index 70% rename from CliFx.Tests/CommandOptionConverterTests.cs rename to CliFx.Tests/CommandOptionInputConverterTests.cs index 2bd332a..035ee45 100644 --- a/CliFx.Tests/CommandOptionConverterTests.cs +++ b/CliFx.Tests/CommandOptionInputConverterTests.cs @@ -7,7 +7,7 @@ using NUnit.Framework; namespace CliFx.Tests { - public partial class CommandOptionConverterTests + public partial class CommandOptionInputConverterTests { public enum TestEnum { @@ -40,162 +40,162 @@ namespace CliFx.Tests } [TestFixture] - public partial class CommandOptionConverterTests + public partial class CommandOptionInputConverterTests { private static IEnumerable GetTestCases_ConvertOption() { yield return new TestCaseData( - new CommandOption("option", "value"), + new CommandOptionInput("option", "value"), typeof(string), "value" ); yield return new TestCaseData( - new CommandOption("option", "value"), + new CommandOptionInput("option", "value"), typeof(object), "value" ); yield return new TestCaseData( - new CommandOption("option", "true"), + new CommandOptionInput("option", "true"), typeof(bool), true ); yield return new TestCaseData( - new CommandOption("option", "false"), + new CommandOptionInput("option", "false"), typeof(bool), false ); yield return new TestCaseData( - new CommandOption("option"), + new CommandOptionInput("option"), typeof(bool), true ); yield return new TestCaseData( - new CommandOption("option", "123"), + new CommandOptionInput("option", "123"), typeof(int), 123 ); yield return new TestCaseData( - new CommandOption("option", "123.45"), + new CommandOptionInput("option", "123.45"), typeof(double), 123.45 ); yield return new TestCaseData( - new CommandOption("option", "28 Apr 1995"), + new CommandOptionInput("option", "28 Apr 1995"), typeof(DateTime), new DateTime(1995, 04, 28) ); yield return new TestCaseData( - new CommandOption("option", "28 Apr 1995"), + new CommandOptionInput("option", "28 Apr 1995"), typeof(DateTimeOffset), new DateTimeOffset(new DateTime(1995, 04, 28)) ); yield return new TestCaseData( - new CommandOption("option", "00:14:59"), + new CommandOptionInput("option", "00:14:59"), typeof(TimeSpan), new TimeSpan(00, 14, 59) ); yield return new TestCaseData( - new CommandOption("option", "value2"), + new CommandOptionInput("option", "value2"), typeof(TestEnum), TestEnum.Value2 ); yield return new TestCaseData( - new CommandOption("option", "666"), + new CommandOptionInput("option", "666"), typeof(int?), 666 ); yield return new TestCaseData( - new CommandOption("option"), + new CommandOptionInput("option"), typeof(int?), null ); yield return new TestCaseData( - new CommandOption("option", "value3"), + new CommandOptionInput("option", "value3"), typeof(TestEnum?), TestEnum.Value3 ); yield return new TestCaseData( - new CommandOption("option"), + new CommandOptionInput("option"), typeof(TestEnum?), null ); yield return new TestCaseData( - new CommandOption("option", "01:00:00"), + new CommandOptionInput("option", "01:00:00"), typeof(TimeSpan?), new TimeSpan(01, 00, 00) ); yield return new TestCaseData( - new CommandOption("option"), + new CommandOptionInput("option"), typeof(TimeSpan?), null ); yield return new TestCaseData( - new CommandOption("option", "value"), + new CommandOptionInput("option", "value"), typeof(TestStringConstructable), new TestStringConstructable("value") ); yield return new TestCaseData( - new CommandOption("option", "value"), + new CommandOptionInput("option", "value"), typeof(TestStringParseable), TestStringParseable.Parse("value") ); yield return new TestCaseData( - new CommandOption("option", new[] {"value1", "value2"}), + new CommandOptionInput("option", new[] {"value1", "value2"}), typeof(string[]), new[] {"value1", "value2"} ); yield return new TestCaseData( - new CommandOption("option", new[] {"value1", "value2"}), + new CommandOptionInput("option", new[] {"value1", "value2"}), typeof(object[]), new[] {"value1", "value2"} ); yield return new TestCaseData( - new CommandOption("option", new[] {"47", "69"}), + new CommandOptionInput("option", new[] {"47", "69"}), typeof(int[]), new[] {47, 69} ); yield return new TestCaseData( - new CommandOption("option", new[] {"value1", "value3"}), + new CommandOptionInput("option", new[] {"value1", "value3"}), typeof(TestEnum[]), new[] {TestEnum.Value1, TestEnum.Value3} ); yield return new TestCaseData( - new CommandOption("option", new[] {"value1", "value2"}), + new CommandOptionInput("option", new[] {"value1", "value2"}), typeof(IEnumerable), new[] {"value1", "value2"} ); yield return new TestCaseData( - new CommandOption("option", new[] {"value1", "value2"}), + new CommandOptionInput("option", new[] {"value1", "value2"}), typeof(IEnumerable), new[] {"value1", "value2"} ); yield return new TestCaseData( - new CommandOption("option", new[] {"value1", "value2"}), + new CommandOptionInput("option", new[] {"value1", "value2"}), typeof(IReadOnlyList), new[] {"value1", "value2"} ); @@ -203,13 +203,13 @@ namespace CliFx.Tests [Test] [TestCaseSource(nameof(GetTestCases_ConvertOption))] - public void ConvertOption_Test(CommandOption option, Type targetType, object expectedConvertedValue) + public void ConvertOption_Test(CommandOptionInput optionInput, Type targetType, object expectedConvertedValue) { // Arrange - var converter = new CommandOptionConverter(); + var converter = new CommandOptionInputConverter(); // Act - var convertedValue = converter.ConvertOption(option, targetType); + var convertedValue = converter.ConvertOption(optionInput, targetType); // Assert Assert.Multiple(() => diff --git a/CliFx.Tests/CommandOptionParserTests.cs b/CliFx.Tests/CommandOptionParserTests.cs deleted file mode 100644 index 1167be5..0000000 --- a/CliFx.Tests/CommandOptionParserTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Collections.Generic; -using CliFx.Models; -using CliFx.Services; -using NUnit.Framework; - -namespace CliFx.Tests -{ - [TestFixture] - public class CommandOptionParserTests - { - private static IEnumerable GetTestCases_ParseOptions() - { - yield return new TestCaseData(new string[0], CommandOptionSet.Empty); - - yield return new TestCaseData( - new[] {"--option", "value"}, - new CommandOptionSet(new[] - { - new CommandOption("option", "value") - }) - ); - - yield return new TestCaseData( - new[] {"--option1", "value1", "--option2", "value2"}, - new CommandOptionSet(new[] - { - new CommandOption("option1", "value1"), - new CommandOption("option2", "value2") - }) - ); - - yield return new TestCaseData( - new[] {"--option", "value1", "value2"}, - new CommandOptionSet(new[] - { - new CommandOption("option", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"--option", "value1", "--option", "value2"}, - new CommandOptionSet(new[] - { - new CommandOption("option", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value"}, - new CommandOptionSet(new[] - { - new CommandOption("a", "value") - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value1", "-b", "value2"}, - new CommandOptionSet(new[] - { - new CommandOption("a", "value1"), - new CommandOption("b", "value2") - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value1", "value2"}, - new CommandOptionSet(new[] - { - new CommandOption("a", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value1", "-a", "value2"}, - new CommandOptionSet(new[] - { - new CommandOption("a", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"--option1", "value1", "-b", "value2"}, - new CommandOptionSet(new[] - { - new CommandOption("option1", "value1"), - new CommandOption("b", "value2") - }) - ); - - yield return new TestCaseData( - new[] {"--switch"}, - new CommandOptionSet(new[] - { - new CommandOption("switch") - }) - ); - - yield return new TestCaseData( - new[] {"--switch1", "--switch2"}, - new CommandOptionSet(new[] - { - new CommandOption("switch1"), - new CommandOption("switch2") - }) - ); - - yield return new TestCaseData( - new[] {"-s"}, - new CommandOptionSet(new[] - { - new CommandOption("s") - }) - ); - - yield return new TestCaseData( - new[] {"-a", "-b"}, - new CommandOptionSet(new[] - { - new CommandOption("a"), - new CommandOption("b") - }) - ); - - yield return new TestCaseData( - new[] {"-ab"}, - new CommandOptionSet(new[] - { - new CommandOption("a"), - new CommandOption("b") - }) - ); - - yield return new TestCaseData( - new[] {"-ab", "value"}, - new CommandOptionSet(new[] - { - new CommandOption("a"), - new CommandOption("b", "value") - }) - ); - - yield return new TestCaseData( - new[] {"command"}, - new CommandOptionSet("command") - ); - - yield return new TestCaseData( - new[] {"command", "--option", "value"}, - new CommandOptionSet("command", new[] - { - new CommandOption("option", "value") - }) - ); - } - - [Test] - [TestCaseSource(nameof(GetTestCases_ParseOptions))] - public void ParseOptions_Test(IReadOnlyList commandLineArguments, CommandOptionSet expectedCommandOptionSet) - { - // Arrange - var parser = new CommandOptionParser(); - - // Act - var optionSet = parser.ParseOptions(commandLineArguments); - - // Assert - Assert.Multiple(() => - { - Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName), "Command name"); - Assert.That(optionSet.Options.Count, Is.EqualTo(expectedCommandOptionSet.Options.Count), "Option count"); - - for (var i = 0; i < optionSet.Options.Count; i++) - { - Assert.That(optionSet.Options[i].Name, Is.EqualTo(expectedCommandOptionSet.Options[i].Name), - $"Option[{i}] name"); - - Assert.That(optionSet.Options[i].Values, Is.EqualTo(expectedCommandOptionSet.Options[i].Values), - $"Option[{i}] values"); - } - }); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/CommandResolverTests.cs b/CliFx.Tests/CommandResolverTests.cs deleted file mode 100644 index 2340b78..0000000 --- a/CliFx.Tests/CommandResolverTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using CliFx.Attributes; -using CliFx.Exceptions; -using CliFx.Models; -using CliFx.Services; -using NUnit.Framework; - -namespace CliFx.Tests -{ - public partial class CommandResolverTests - { - [DefaultCommand] - public class TestCommand : Command - { - [CommandOption("int", 'i', IsRequired = true)] - public int IntOption { get; set; } = 24; - - [CommandOption("str", 's')] public string StringOption { get; set; } = "foo bar"; - - public override ExitCode Execute() => new ExitCode(IntOption, StringOption); - } - } - - [TestFixture] - public partial class CommandResolverTests - { - private static IEnumerable GetTestCases_ResolveCommand() - { - yield return new TestCaseData( - new CommandOptionSet(new[] - { - new CommandOption("int", "13") - }), - new TestCommand {IntOption = 13} - ); - - yield return new TestCaseData( - new CommandOptionSet(new[] - { - new CommandOption("int", "13"), - new CommandOption("str", "hello world") - }), - new TestCommand {IntOption = 13, StringOption = "hello world"} - ); - - yield return new TestCaseData( - new CommandOptionSet(new[] - { - new CommandOption("i", "13") - }), - new TestCommand {IntOption = 13} - ); - } - - [Test] - [TestCaseSource(nameof(GetTestCases_ResolveCommand))] - public void ResolveCommand_Test(CommandOptionSet commandOptionSet, TestCommand expectedCommand) - { - // Arrange - var resolver = new CommandResolver(new[] {typeof(TestCommand)}, new CommandOptionConverter()); - - // Act - var command = resolver.ResolveCommand(commandOptionSet) as TestCommand; - - // Assert - Assert.Multiple(() => - { - 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 GetTestCases_ResolveCommand_IsRequired() - { - yield return new TestCaseData(CommandOptionSet.Empty); - - yield return new TestCaseData( - new CommandOptionSet(new[] - { - new CommandOption("str", "hello world") - }) - ); - } - - [Test] - [TestCaseSource(nameof(GetTestCases_ResolveCommand_IsRequired))] - public void ResolveCommand_IsRequired_Test(CommandOptionSet commandOptionSet) - { - // Arrange - var resolver = new CommandResolver(new[] {typeof(TestCommand)}, new CommandOptionConverter()); - - // Act & Assert - Assert.Throws(() => resolver.ResolveCommand(commandOptionSet)); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/CommandSchemaResolverTests.cs b/CliFx.Tests/CommandSchemaResolverTests.cs new file mode 100644 index 0000000..2f2ab36 --- /dev/null +++ b/CliFx.Tests/CommandSchemaResolverTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Models; +using CliFx.Services; +using NUnit.Framework; + +namespace CliFx.Tests +{ + public partial class CommandSchemaResolverTests + { + [Command(Description = "Command description")] + public class TestCommand : ICommand + { + [CommandOption("option-a", 'a', GroupName = "Group 1")] + public int OptionA { get; set; } + + [CommandOption("option-b", IsRequired = true)] + public string OptionB { get; set; } + + [CommandOption("option-c", Description = "Option C description")] + public bool OptionC { get; set; } + + public CommandContext Context { get; set; } + + public Task ExecuteAsync() => throw new NotImplementedException(); + } + } + + [TestFixture] + public partial class CommandSchemaResolverTests + { + private static IEnumerable GetTestCases_ResolveAllSchemas() + { + yield return new TestCaseData( + new[] {typeof(TestCommand)}, + new[] + { + new CommandSchema(typeof(TestCommand), + null, true, "Command description", + new[] + { + new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionA)), + "option-a", 'a', false, "Group 1", null), + new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionB)), + "option-b", null, true, null, null), + new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionC)), + "option-c", null, false, null, "Option C description") + }) + } + ); + } + + [Test] + [TestCaseSource(nameof(GetTestCases_ResolveAllSchemas))] + public void ResolveAllSchemas_Test(IReadOnlyList sourceTypes, IReadOnlyList expectedSchemas) + { + // Arrange + var resolver = new CommandSchemaResolver(sourceTypes); + + // Act + var schemas = resolver.ResolveAllSchemas(); + + // Assert + Assert.That(schemas, Is.EqualTo(expectedSchemas).Using(CommandSchemaEqualityComparer.Instance)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/DummyTests.cs b/CliFx.Tests/DummyTests.cs index d2e89ba..7b334c1 100644 --- a/CliFx.Tests/DummyTests.cs +++ b/CliFx.Tests/DummyTests.cs @@ -9,6 +9,8 @@ namespace CliFx.Tests { private static string DummyFilePath => typeof(Dummy.Program).Assembly.Location; + private static string DummyVersionText => typeof(Dummy.Program).Assembly.GetName().Version.ToString(); + [Test] [TestCase("", "Hello world")] [TestCase("-t .NET", "Hello .NET")] @@ -18,15 +20,55 @@ namespace CliFx.Tests [TestCase("add -v 4 -v 16", "20")] [TestCase("log -v 100", "2")] [TestCase("log --value 256 --base 2", "8")] - public async Task Execute_Test(string arguments, string expectedOutput) + public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput) { // Act var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); // Assert - Assert.That(result.ExitCode, Is.Zero, "Exit code"); - Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout"); - Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr"); + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.Zero, "Exit code"); + Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout"); + Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr"); + }); + } + + [Test] + [TestCase("--version")] + public async Task CliApplication_RunAsync_Version_Test(string arguments) + { + // Act + var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); + + // Assert + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.Zero, "Exit code"); + Assert.That(result.StandardOutput.Trim(), Is.EqualTo(DummyVersionText), "Stdout"); + Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr"); + }); + } + + [Test] + [TestCase("--help")] + [TestCase("-h")] + [TestCase("add -h")] + [TestCase("add --help")] + [TestCase("log -h")] + [TestCase("log --help")] + public async Task CliApplication_RunAsync_Help_Test(string arguments) + { + // Act + var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); + + // Assert + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.Zero, "Exit code"); + Assert.That(result.StandardOutput.Trim(), Is.Not.Empty, "Stdout"); + Assert.That(result.StandardError.Trim(), Is.Empty, "Stderr"); + }); } } } \ No newline at end of file diff --git a/CliFx/Attributes/CommandAttribute.cs b/CliFx/Attributes/CommandAttribute.cs index 4362401..5c88b58 100644 --- a/CliFx/Attributes/CommandAttribute.cs +++ b/CliFx/Attributes/CommandAttribute.cs @@ -1,4 +1,5 @@ using System; +using CliFx.Internal; namespace CliFx.Attributes { @@ -9,9 +10,16 @@ namespace CliFx.Attributes public string Description { get; set; } + public bool IsDefault => Name.IsNullOrWhiteSpace(); + public CommandAttribute(string name) { Name = name; } + + public CommandAttribute() + : this(null) + { + } } } \ No newline at end of file diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs index f838249..36eea04 100644 --- a/CliFx/Attributes/CommandOptionAttribute.cs +++ b/CliFx/Attributes/CommandOptionAttribute.cs @@ -11,6 +11,8 @@ namespace CliFx.Attributes public bool IsRequired { get; set; } + public string GroupName { get; set; } + public string Description { get; set; } public CommandOptionAttribute(string name, char? shortName) diff --git a/CliFx/Attributes/DefaultCommandAttribute.cs b/CliFx/Attributes/DefaultCommandAttribute.cs deleted file mode 100644 index be22488..0000000 --- a/CliFx/Attributes/DefaultCommandAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace CliFx.Attributes -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public class DefaultCommandAttribute : CommandAttribute - { - public DefaultCommandAttribute() - : base(null) - { - } - } -} \ No newline at end of file diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 32cc0dd..4fd04e4 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -6,24 +6,24 @@ namespace CliFx { public class CliApplication : ICliApplication { - private readonly ICommandOptionParser _commandOptionParser; - private readonly ICommandResolver _commandResolver; + private readonly ICommandInputParser _commandInputParser; + private readonly ICommandInitializer _commandInitializer; - public CliApplication(ICommandOptionParser commandOptionParser, ICommandResolver commandResolver) + public CliApplication(ICommandInputParser commandInputParser, ICommandInitializer commandInitializer) { - _commandOptionParser = commandOptionParser; - _commandResolver = commandResolver; + _commandInputParser = commandInputParser; + _commandInitializer = commandInitializer; } public CliApplication() - : this(new CommandOptionParser(), new CommandResolver(new CommandOptionConverter())) + : this(new CommandInputParser(), new CommandInitializer()) { } public async Task RunAsync(IReadOnlyList commandLineArguments) { - var optionSet = _commandOptionParser.ParseOptions(commandLineArguments); - var command = _commandResolver.ResolveCommand(optionSet); + var input = _commandInputParser.ParseInput(commandLineArguments); + var command = _commandInitializer.InitializeCommand(input); var exitCode = await command.ExecuteAsync(); diff --git a/CliFx/Command.cs b/CliFx/Command.cs index f8797c3..4eedc9a 100644 --- a/CliFx/Command.cs +++ b/CliFx/Command.cs @@ -1,15 +1,59 @@ using System; +using System.Reflection; using System.Threading.Tasks; +using CliFx.Attributes; using CliFx.Models; +using CliFx.Services; namespace CliFx { public abstract class Command : ICommand { - 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."); + [CommandOption("help", 'h', GroupName = "__help", Description = "Shows help.")] + public bool IsHelpRequested { get; set; } - public virtual Task ExecuteAsync() => Task.FromResult(Execute()); + [CommandOption("version", GroupName = "__version", Description = "Shows application version.")] + public bool IsVersionRequested { get; set; } + + public CommandContext Context { get; set; } + + public IConsoleWriter Output { get; set; } = ConsoleWriter.GetStandardOutput(); + + public IConsoleWriter Error { get; set; } = ConsoleWriter.GetStandardError(); + + protected virtual ExitCode Process() => throw new InvalidOperationException( + "Can't execute command because its execution method is not defined. " + + $"Override {nameof(Process)} or {nameof(ProcessAsync)} on {GetType().Name} in order to make it executable."); + + protected virtual Task ProcessAsync() => Task.FromResult(Process()); + + protected virtual void ShowHelp() + { + var text = new HelpTextBuilder().Build(Context); + Output.WriteLine(text); + } + + protected virtual void ShowVersion() + { + var text = Assembly.GetEntryAssembly()?.GetName().Version.ToString(); + Output.WriteLine(text); + } + + public Task ExecuteAsync() + { + if (IsHelpRequested) + { + ShowHelp(); + return Task.FromResult(ExitCode.Success); + } + + if (IsVersionRequested && Context.CommandSchema.IsDefault) + { + ShowVersion(); + return Task.FromResult(ExitCode.Success); + } + + return ProcessAsync(); + } } } \ No newline at end of file diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs index 8cbaccd..7e21993 100644 --- a/CliFx/ICommand.cs +++ b/CliFx/ICommand.cs @@ -5,6 +5,8 @@ namespace CliFx { public interface ICommand { + CommandContext Context { get; set; } + Task ExecuteAsync(); } } \ No newline at end of file diff --git a/CliFx/Internal/CommandOptionProperty.cs b/CliFx/Internal/CommandOptionProperty.cs deleted file mode 100644 index 1c5be2a..0000000 --- a/CliFx/Internal/CommandOptionProperty.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 609d710..0000000 --- a/CliFx/Internal/CommandType.cs +++ /dev/null @@ -1,60 +0,0 @@ -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 string Description { get; } - - public IReadOnlyList Options { get; } - - public CommandType(Type type, string name, bool isDefault, string description, IReadOnlyList options) - { - _type = type; - Name = name; - IsDefault = isDefault; - Description = description; - Options = options; - } - - public Command Activate() => (Command) Activator.CreateInstance(_type); - } - - internal partial class CommandType - { - public static bool IsValid(Type type) => - type.GetInterfaces().Contains(typeof(ICommand)) && - 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 attribute = type.GetCustomAttribute(); - - var name = attribute.Name; - var isDefault = attribute is DefaultCommandAttribute; - var description = attribute.Description; - - var options = type.GetProperties() - .Where(CommandOptionProperty.IsValid) - .Select(CommandOptionProperty.Initialize) - .ToArray(); - - return new CommandType(type, name, isDefault, description, options); - } - - public static IEnumerable GetCommandTypes(IEnumerable types) => types.Where(IsValid).Select(Initialize); - } -} \ No newline at end of file diff --git a/CliFx/Internal/HashCodeBuilder.cs b/CliFx/Internal/HashCodeBuilder.cs new file mode 100644 index 0000000..6395346 --- /dev/null +++ b/CliFx/Internal/HashCodeBuilder.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +namespace CliFx.Internal +{ + internal class HashCodeBuilder + { + private int _code = 17; + + public HashCodeBuilder Add(int hashCode) + { + unchecked + { + _code = _code * 23 + hashCode; + } + + return this; + } + + public HashCodeBuilder Add(IEnumerable hashCodes) + { + foreach (var hashCode in hashCodes) + Add(hashCode); + + return this; + } + + public HashCodeBuilder Add(T obj, IEqualityComparer comparer) => Add(comparer.GetHashCode(obj)); + + public HashCodeBuilder Add(T obj) => Add(obj, EqualityComparer.Default); + + public HashCodeBuilder AddMany(IEnumerable objs, IEqualityComparer comparer) => Add(objs.Select(comparer.GetHashCode)); + + public HashCodeBuilder AddMany(IEnumerable objs) => AddMany(objs, EqualityComparer.Default); + + public int Build() => _code; + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandContext.cs b/CliFx/Models/CommandContext.cs new file mode 100644 index 0000000..7f06661 --- /dev/null +++ b/CliFx/Models/CommandContext.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace CliFx.Models +{ + public class CommandContext + { + public IReadOnlyList AvailableCommandSchemas { get; } + + public CommandSchema CommandSchema { get; } + + public CommandContext(IReadOnlyList availableCommandSchemas, CommandSchema commandSchema) + { + AvailableCommandSchemas = availableCommandSchemas; + CommandSchema = commandSchema; + } + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandInput.cs b/CliFx/Models/CommandInput.cs new file mode 100644 index 0000000..72ca70a --- /dev/null +++ b/CliFx/Models/CommandInput.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CliFx.Internal; + +namespace CliFx.Models +{ + public partial class CommandInput + { + public string CommandName { get; } + + public IReadOnlyList Options { get; } + + public CommandInput(string commandName, IReadOnlyList options) + { + CommandName = commandName; + Options = options; + } + + public CommandInput(IReadOnlyList options) + : this(null, options) + { + } + + public CommandInput(string commandName) + : this(commandName, new CommandOptionInput[0]) + { + } + + public CommandInput() + : this(null, new CommandOptionInput[0]) + { + } + + public override string ToString() + { + var buffer = new StringBuilder(); + + if (!CommandName.IsNullOrWhiteSpace()) + { + buffer.Append(CommandName); + } + + if (Options.Any()) + { + if (buffer.Length > 0) + buffer.Append(' '); + + buffer.Append('['); + + foreach (var option in Options) + { + buffer.Append(option.Name); + } + + buffer.Append(']'); + } + + return buffer.ToString(); + } + } + + public partial class CommandInput + { + public static CommandInput Empty { get; } = new CommandInput(); + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandInputEqualityComparer.cs b/CliFx/Models/CommandInputEqualityComparer.cs new file mode 100644 index 0000000..9f0f550 --- /dev/null +++ b/CliFx/Models/CommandInputEqualityComparer.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CliFx.Internal; + +namespace CliFx.Models +{ + public partial class CommandInputEqualityComparer : IEqualityComparer + { + /// + public bool Equals(CommandInput x, CommandInput y) + { + if (ReferenceEquals(x, y)) + return true; + + if (x is null || y is null) + return false; + + return StringComparer.OrdinalIgnoreCase.Equals(x.CommandName, y.CommandName) && + x.Options.SequenceEqual(y.Options, CommandOptionInputEqualityComparer.Instance); + } + + /// + public int GetHashCode(CommandInput obj) => new HashCodeBuilder() + .Add(obj.CommandName, StringComparer.OrdinalIgnoreCase) + .AddMany(obj.Options, CommandOptionInputEqualityComparer.Instance) + .Build(); + } + + public partial class CommandInputEqualityComparer + { + public static CommandInputEqualityComparer Instance { get; } = new CommandInputEqualityComparer(); + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandOption.cs b/CliFx/Models/CommandOptionInput.cs similarity index 62% rename from CliFx/Models/CommandOption.cs rename to CliFx/Models/CommandOptionInput.cs index 2362cb6..19f1646 100644 --- a/CliFx/Models/CommandOption.cs +++ b/CliFx/Models/CommandOptionInput.cs @@ -2,24 +2,24 @@ namespace CliFx.Models { - public class CommandOption + public class CommandOptionInput { public string Name { get; } public IReadOnlyList Values { get; } - public CommandOption(string name, IReadOnlyList values) + public CommandOptionInput(string name, IReadOnlyList values) { Name = name; Values = values; } - public CommandOption(string name, string value) + public CommandOptionInput(string name, string value) : this(name, new[] {value}) { } - public CommandOption(string name) + public CommandOptionInput(string name) : this(name, new string[0]) { } diff --git a/CliFx/Models/CommandOptionInputEqualityComparer.cs b/CliFx/Models/CommandOptionInputEqualityComparer.cs new file mode 100644 index 0000000..7b9d358 --- /dev/null +++ b/CliFx/Models/CommandOptionInputEqualityComparer.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CliFx.Internal; + +namespace CliFx.Models +{ + public partial class CommandOptionInputEqualityComparer : IEqualityComparer + { + /// + public bool Equals(CommandOptionInput x, CommandOptionInput y) + { + if (ReferenceEquals(x, y)) + return true; + + if (x is null || y is null) + return false; + + return StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) && + x.Values.SequenceEqual(y.Values, StringComparer.Ordinal); + } + + /// + public int GetHashCode(CommandOptionInput obj) => new HashCodeBuilder() + .Add(obj.Name, StringComparer.OrdinalIgnoreCase) + .AddMany(obj.Values, StringComparer.Ordinal) + .Build(); + } + + public partial class CommandOptionInputEqualityComparer + { + public static CommandOptionInputEqualityComparer Instance { get; } = new CommandOptionInputEqualityComparer(); + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandOptionSchema.cs b/CliFx/Models/CommandOptionSchema.cs new file mode 100644 index 0000000..6e5af5f --- /dev/null +++ b/CliFx/Models/CommandOptionSchema.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Reflection; +using CliFx.Internal; + +namespace CliFx.Models +{ + public class CommandOptionSchema + { + public PropertyInfo Property { get; } + + public string Name { get; } + + public char? ShortName { get; } + + public bool IsRequired { get; } + + public string GroupName { get; } + + public string Description { get; } + + public CommandOptionSchema(PropertyInfo property, string name, char? shortName, + bool isRequired, string groupName, string description) + { + Property = property; + Name = name; + ShortName = shortName; + IsRequired = isRequired; + GroupName = groupName; + Description = description; + } + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandOptionSchemaEqualityComparer.cs b/CliFx/Models/CommandOptionSchemaEqualityComparer.cs new file mode 100644 index 0000000..c889957 --- /dev/null +++ b/CliFx/Models/CommandOptionSchemaEqualityComparer.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using CliFx.Internal; + +namespace CliFx.Models +{ + public partial class CommandOptionSchemaEqualityComparer : IEqualityComparer + { + /// + public bool Equals(CommandOptionSchema x, CommandOptionSchema y) + { + if (ReferenceEquals(x, y)) + return true; + + if (x is null || y is null) + return false; + + return x.Property == y.Property && + StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) && + x.ShortName == y.ShortName && + StringComparer.OrdinalIgnoreCase.Equals(x.GroupName, y.GroupName) && + StringComparer.Ordinal.Equals(x.Description, y.Description); + } + + /// + public int GetHashCode(CommandOptionSchema obj) => new HashCodeBuilder() + .Add(obj.Property) + .Add(obj.Name, StringComparer.OrdinalIgnoreCase) + .Add(obj.ShortName) + .Add(obj.GroupName, StringComparer.OrdinalIgnoreCase) + .Add(obj.Description, StringComparer.Ordinal) + .Build(); + } + + public partial class CommandOptionSchemaEqualityComparer + { + public static CommandOptionSchemaEqualityComparer Instance { get; } = new CommandOptionSchemaEqualityComparer(); + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandOptionSet.cs b/CliFx/Models/CommandOptionSet.cs deleted file mode 100644 index 6bb91ae..0000000 --- a/CliFx/Models/CommandOptionSet.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using CliFx.Internal; - -namespace CliFx.Models -{ - public partial class CommandOptionSet - { - public string CommandName { get; } - - public IReadOnlyList Options { get; } - - public CommandOptionSet(string commandName, IReadOnlyList options) - { - CommandName = commandName; - Options = options; - } - - public CommandOptionSet(IReadOnlyList options) - : this(null, options) - { - } - - public CommandOptionSet(string commandName) - : this(commandName, new CommandOption[0]) - { - } - - public override string ToString() - { - if (Options.Any()) - { - var optionsJoined = Options.Select(o => o.Name).JoinToString(", "); - return !CommandName.IsNullOrWhiteSpace() ? $"{CommandName} / [{optionsJoined}]" : $"[{optionsJoined}]"; - } - else - { - return !CommandName.IsNullOrWhiteSpace() ? $"{CommandName} / no options" : "no options"; - } - } - } - - public partial class CommandOptionSet - { - public static CommandOptionSet Empty { get; } = new CommandOptionSet(new CommandOption[0]); - } -} \ No newline at end of file diff --git a/CliFx/Models/CommandSchema.cs b/CliFx/Models/CommandSchema.cs new file mode 100644 index 0000000..a4f8dea --- /dev/null +++ b/CliFx/Models/CommandSchema.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace CliFx.Models +{ + public class CommandSchema + { + public Type Type { get; } + + public string Name { get; } + + public bool IsDefault { get; } + + public string Description { get; } + + public IReadOnlyList Options { get; } + + public CommandSchema(Type type, string name, bool isDefault, string description, IReadOnlyList options) + { + Type = type; + Name = name; + IsDefault = isDefault; + Description = description; + Options = options; + } + } +} \ No newline at end of file diff --git a/CliFx/Models/CommandSchemaEqualityComparer.cs b/CliFx/Models/CommandSchemaEqualityComparer.cs new file mode 100644 index 0000000..0e7d809 --- /dev/null +++ b/CliFx/Models/CommandSchemaEqualityComparer.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CliFx.Internal; + +namespace CliFx.Models +{ + public partial class CommandSchemaEqualityComparer : IEqualityComparer + { + /// + public bool Equals(CommandSchema x, CommandSchema y) + { + if (ReferenceEquals(x, y)) + return true; + + if (x is null || y is null) + return false; + + return x.Type == y.Type && + StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) && + x.IsDefault == y.IsDefault && + StringComparer.Ordinal.Equals(x.Description, y.Description) && + x.Options.SequenceEqual(y.Options, CommandOptionSchemaEqualityComparer.Instance); + } + + /// + public int GetHashCode(CommandSchema obj) => new HashCodeBuilder() + .Add(obj.Type) + .Add(obj.Name, StringComparer.OrdinalIgnoreCase) + .Add(obj.IsDefault) + .Add(obj.Description, StringComparer.Ordinal) + .AddMany(obj.Options, CommandOptionSchemaEqualityComparer.Instance) + .Build(); + } + + public partial class CommandSchemaEqualityComparer + { + public static CommandSchemaEqualityComparer Instance { get; } = new CommandSchemaEqualityComparer(); + } +} \ No newline at end of file diff --git a/CliFx/Models/Extensions.cs b/CliFx/Models/Extensions.cs index 62b030a..f12585f 100644 --- a/CliFx/Models/Extensions.cs +++ b/CliFx/Models/Extensions.cs @@ -6,7 +6,7 @@ namespace CliFx.Models { public static class Extensions { - public static CommandOption GetOptionOrDefault(this CommandOptionSet set, string name, char? shortName) => + public static CommandOptionInput GetOptionOrDefault(this CommandInput set, string name, char? shortName) => set.Options.FirstOrDefault(o => { if (!name.IsNullOrWhiteSpace() && string.Equals(o.Name, name, StringComparison.Ordinal)) diff --git a/CliFx/Models/TextSpan.cs b/CliFx/Models/TextSpan.cs new file mode 100644 index 0000000..7c349f4 --- /dev/null +++ b/CliFx/Models/TextSpan.cs @@ -0,0 +1,24 @@ +using System.Drawing; + +namespace CliFx.Models +{ + public class TextSpan + { + public string Text { get; } + + public Color Color { get; } + + public TextSpan(string text, Color color) + { + Text = text; + Color = color; + } + + public TextSpan(string text) + : this(text, Color.Gray) + { + } + + public override string ToString() => Text; + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandInitializer.cs b/CliFx/Services/CommandInitializer.cs new file mode 100644 index 0000000..002f67e --- /dev/null +++ b/CliFx/Services/CommandInitializer.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CliFx.Attributes; +using CliFx.Exceptions; +using CliFx.Internal; +using CliFx.Models; + +namespace CliFx.Services +{ + public class CommandInitializer : ICommandInitializer + { + private readonly ITypeActivator _typeActivator; + private readonly ICommandSchemaResolver _commandSchemaResolver; + private readonly ICommandOptionInputConverter _commandOptionInputConverter; + + public CommandInitializer(ITypeActivator typeActivator, ICommandSchemaResolver commandSchemaResolver, + ICommandOptionInputConverter commandOptionInputConverter) + { + _typeActivator = typeActivator; + _commandSchemaResolver = commandSchemaResolver; + _commandOptionInputConverter = commandOptionInputConverter; + } + + public CommandInitializer(ICommandSchemaResolver commandSchemaResolver) + : this(new TypeActivator(), commandSchemaResolver, new CommandOptionInputConverter()) + { + } + + public CommandInitializer() + : this(new CommandSchemaResolver()) + { + } + + private CommandSchema GetDefaultSchema(IReadOnlyList schemas) + { + // Get command types marked as default + var defaultSchemas = schemas.Where(t => t.IsDefault).ToArray(); + + // If there's only one type - return + if (defaultSchemas.Length == 1) + return defaultSchemas.Single(); + + // If there are multiple - throw + if (defaultSchemas.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(CommandAttribute)} 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(CommandAttribute)} to the default command."); + } + + private CommandSchema GetSchemaByName(IReadOnlyList schemas, string name) + { + // Get command types with given name + var matchingSchemas = + schemas.Where(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)).ToArray(); + + // If there's only one type - return + if (matchingSchemas.Length == 1) + return matchingSchemas.Single(); + + // If there are multiple - throw + if (matchingSchemas.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."); + } + + // TODO: refactor + public ICommand InitializeCommand(CommandInput input) + { + var schemas = _commandSchemaResolver.ResolveAllSchemas(); + + // Get command type + var schema = !input.CommandName.IsNullOrWhiteSpace() + ? GetSchemaByName(schemas, input.CommandName) + : GetDefaultSchema(schemas); + + // Activate command + var command = (ICommand) _typeActivator.Activate(schema.Type); + command.Context = new CommandContext(schemas, schema); + + // Set command options + var isGroupNameDetected = false; + var groupName = default(string); + var properties = new HashSet(); + foreach (var option in input.Options) + { + var optionInfo = schema.Options.FirstOrDefault(p => + string.Equals(p.Name, option.Name, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.ShortName?.AsString(), option.Name, StringComparison.OrdinalIgnoreCase)); + + if (optionInfo == null) + continue; + + if (isGroupNameDetected && !string.Equals(groupName, optionInfo.GroupName, StringComparison.OrdinalIgnoreCase)) + continue; + + if (!isGroupNameDetected) + { + groupName = optionInfo.GroupName; + isGroupNameDetected = true; + } + + var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionInfo.Property.PropertyType); + optionInfo.Property.SetValue(command, convertedValue); + + properties.Add(optionInfo); + } + + var unsetRequiredOptions = schema.Options + .Except(properties) + .Where(p => p.IsRequired) + .Where(p => string.Equals(p.GroupName, groupName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + if (unsetRequiredOptions.Any()) + throw new CommandResolveException( + $"Can't resolve command because one or more required properties were not set: {unsetRequiredOptions.Select(p => p.Name).JoinToString(", ")}"); + + return command; + } + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandOptionParser.cs b/CliFx/Services/CommandInputParser.cs similarity index 89% rename from CliFx/Services/CommandOptionParser.cs rename to CliFx/Services/CommandInputParser.cs index 8238599..dc4e0c2 100644 --- a/CliFx/Services/CommandOptionParser.cs +++ b/CliFx/Services/CommandInputParser.cs @@ -6,9 +6,10 @@ using CliFx.Models; namespace CliFx.Services { - public class CommandOptionParser : ICommandOptionParser + public class CommandInputParser : ICommandInputParser { - public CommandOptionSet ParseOptions(IReadOnlyList commandLineArguments) + // TODO: refactor + public CommandInput ParseInput(IReadOnlyList commandLineArguments) { // Initialize command name placeholder string commandName = null; @@ -71,7 +72,7 @@ namespace CliFx.Services isFirstArgument = false; } - return new CommandOptionSet(commandName, rawOptions.Select(p => new CommandOption(p.Key, p.Value)).ToArray()); + return new CommandInput(commandName, rawOptions.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray()); } } } \ No newline at end of file diff --git a/CliFx/Services/CommandOptionConverter.cs b/CliFx/Services/CommandOptionInputConverter.cs similarity index 96% rename from CliFx/Services/CommandOptionConverter.cs rename to CliFx/Services/CommandOptionInputConverter.cs index 861fe05..be8750b 100644 --- a/CliFx/Services/CommandOptionConverter.cs +++ b/CliFx/Services/CommandOptionInputConverter.cs @@ -8,16 +8,16 @@ using CliFx.Models; namespace CliFx.Services { - public class CommandOptionConverter : ICommandOptionConverter + public class CommandOptionInputConverter : ICommandOptionInputConverter { private readonly IFormatProvider _formatProvider; - public CommandOptionConverter(IFormatProvider formatProvider) + public CommandOptionInputConverter(IFormatProvider formatProvider) { _formatProvider = formatProvider; } - public CommandOptionConverter() + public CommandOptionInputConverter() : this(CultureInfo.InvariantCulture) { } @@ -216,7 +216,8 @@ namespace CliFx.Services throw new CommandOptionConvertException($"Can't convert value [{value}] to unrecognized type [{targetType}]."); } - public object ConvertOption(CommandOption option, Type targetType) + // TODO: refactor this + public object ConvertOption(CommandOptionInput option, Type targetType) { if (targetType != typeof(string) && targetType.IsEnumerable()) { diff --git a/CliFx/Services/CommandResolver.cs b/CliFx/Services/CommandResolver.cs deleted file mode 100644 index 029265d..0000000 --- a/CliFx/Services/CommandResolver.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using CliFx.Attributes; -using CliFx.Exceptions; -using CliFx.Internal; -using CliFx.Models; - -namespace CliFx.Services -{ - public partial class CommandResolver : ICommandResolver - { - private readonly IReadOnlyList _availableTypes; - private readonly ICommandOptionConverter _commandOptionConverter; - - public CommandResolver(IReadOnlyList availableTypes, ICommandOptionConverter commandOptionConverter) - { - _availableTypes = availableTypes; - _commandOptionConverter = commandOptionConverter; - } - - public CommandResolver(ICommandOptionConverter commandOptionConverter) - : this(GetDefaultAvailableTypes(), commandOptionConverter) - { - } - - private IEnumerable GetCommandTypes() => CommandType.GetCommandTypes(_availableTypes); - - 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 ICommand ResolveCommand(CommandOptionSet optionSet) - { - // 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.Options) - { - // Get option for this property - var option = optionSet.GetOptionOrDefault(property.Name, property.ShortName); - - // If there are any matching options - set value - if (option != null) - { - var convertedValue = _commandOptionConverter.ConvertOption(option, property.Type); - property.SetValue(command, convertedValue); - } - // If the property is required but it's missing - throw - else if (property.IsRequired) - { - throw new CommandResolveException($"Can't resolve command because required property [{property.Name}] is not set."); - } - } - - return command; - } - } - - public partial class CommandResolver - { - private static IReadOnlyList GetDefaultAvailableTypes() => Assembly.GetEntryAssembly()?.GetExportedTypes() ?? new Type[0]; - } -} \ No newline at end of file diff --git a/CliFx/Services/CommandSchemaResolver.cs b/CliFx/Services/CommandSchemaResolver.cs new file mode 100644 index 0000000..a801035 --- /dev/null +++ b/CliFx/Services/CommandSchemaResolver.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using CliFx.Attributes; +using CliFx.Models; + +namespace CliFx.Services +{ + public class CommandSchemaResolver : ICommandSchemaResolver + { + private readonly IReadOnlyList _sourceTypes; + + public CommandSchemaResolver(IReadOnlyList sourceTypes) + { + _sourceTypes = sourceTypes; + } + + public CommandSchemaResolver(IReadOnlyList sourceAssemblies) + : this(sourceAssemblies.SelectMany(a => a.ExportedTypes).ToArray()) + { + } + + public CommandSchemaResolver() + : this(new[] {Assembly.GetEntryAssembly()}) + { + } + + private IEnumerable GetCommandTypes() => _sourceTypes.Where(t => t.GetInterfaces().Contains(typeof(ICommand))); + + private IReadOnlyList GetCommandOptionSchemas(Type commandType) + { + var result = new List(); + + foreach (var optionProperty in commandType.GetProperties()) + { + var optionAttribute = optionProperty.GetCustomAttribute(); + + if (optionAttribute == null) + continue; + + result.Add(new CommandOptionSchema(optionProperty, + optionAttribute.Name, + optionAttribute.ShortName, + optionAttribute.IsRequired, + optionAttribute.GroupName, + optionAttribute.Description)); + } + + return result; + } + + public IReadOnlyList ResolveAllSchemas() + { + var result = new List(); + + foreach (var commandType in GetCommandTypes()) + { + var commandAttribute = commandType.GetCustomAttribute(); + + if (commandAttribute == null) + continue; + + result.Add(new CommandSchema(commandType, + commandAttribute.Name, + commandAttribute.IsDefault, + commandAttribute.Description, + GetCommandOptionSchemas(commandType))); + } + + return result; + } + } +} \ No newline at end of file diff --git a/CliFx/Services/ConsoleWriter.cs b/CliFx/Services/ConsoleWriter.cs new file mode 100644 index 0000000..3dd9551 --- /dev/null +++ b/CliFx/Services/ConsoleWriter.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using CliFx.Models; + +namespace CliFx.Services +{ + public partial class ConsoleWriter : IConsoleWriter, IDisposable + { + private readonly TextWriter _textWriter; + private readonly bool _isRedirected; + + public ConsoleWriter(TextWriter textWriter, bool isRedirected) + { + _textWriter = textWriter; + _isRedirected = isRedirected; + } + + // TODO: handle colors + public void Write(TextSpan text) => _textWriter.Write(text.Text); + + public void WriteLine(TextSpan text) => _textWriter.WriteLine(text.Text); + + public void Dispose() => _textWriter.Dispose(); + } + + public partial class ConsoleWriter + { + public static ConsoleWriter GetStandardOutput() => new ConsoleWriter(Console.Out, Console.IsOutputRedirected); + + public static ConsoleWriter GetStandardError() => new ConsoleWriter(Console.Error, Console.IsErrorRedirected); + } +} \ No newline at end of file diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs new file mode 100644 index 0000000..8969486 --- /dev/null +++ b/CliFx/Services/Extensions.cs @@ -0,0 +1,11 @@ +using CliFx.Models; + +namespace CliFx.Services +{ + public static class Extensions + { + public static void Write(this IConsoleWriter consoleWriter, string text) => consoleWriter.Write(new TextSpan(text)); + + public static void WriteLine(this IConsoleWriter consoleWriter, string text) => consoleWriter.WriteLine(new TextSpan(text)); + } +} \ No newline at end of file diff --git a/CliFx/Services/HelpTextBuilder.cs b/CliFx/Services/HelpTextBuilder.cs new file mode 100644 index 0000000..7fb5eb3 --- /dev/null +++ b/CliFx/Services/HelpTextBuilder.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using CliFx.Internal; +using CliFx.Models; + +namespace CliFx.Services +{ + // TODO: add color + public class HelpTextBuilder : IHelpTextBuilder + { + // TODO: move to context? + private string GetExeName() => Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()?.Location); + + // TODO: move to context? + private string GetVersionText() => Assembly.GetEntryAssembly()?.GetName().Version.ToString(); + + private IReadOnlyList GetOptionIdentifiers(CommandOptionSchema option) + { + var result = new List(); + + if (option.ShortName != null) + result.Add("-" + option.ShortName.Value); + + if (!option.Name.IsNullOrWhiteSpace()) + result.Add("--" + option.Name); + + return result; + } + + private void AddDescription(StringBuilder buffer, CommandContext context) + { + if (context.CommandSchema.Description.IsNullOrWhiteSpace()) + return; + + buffer.AppendLine("Description:"); + + buffer.Append(" "); + buffer.AppendLine(context.CommandSchema.Description); + + buffer.AppendLine(); + } + + private void AddUsage(StringBuilder buffer, CommandContext context) + { + buffer.AppendLine("Usage:"); + + buffer.Append(" "); + buffer.Append(GetExeName()); + + if (!context.CommandSchema.Name.IsNullOrWhiteSpace()) + { + buffer.Append(' '); + buffer.Append(context.CommandSchema.Name); + } + + if (context.CommandSchema.Options.Any()) + { + buffer.Append(' '); + buffer.Append("[options]"); + } + + buffer.AppendLine().AppendLine(); + } + + private void AddOptions(StringBuilder buffer, CommandContext context) + { + if (!context.CommandSchema.Options.Any()) + return; + + buffer.AppendLine("Options:"); + + foreach (var option in context.CommandSchema.Options) + { + buffer.Append(option.IsRequired ? " * " : " "); + + buffer.Append(GetOptionIdentifiers(option).JoinToString("|")); + + if (!option.Description.IsNullOrWhiteSpace()) + { + buffer.Append(" "); + buffer.Append(option.Description); + } + + buffer.AppendLine(); + } + + buffer.AppendLine(); + } + + public string Build(CommandContext context) + { + var buffer = new StringBuilder(); + + AddDescription(buffer, context); + AddUsage(buffer, context); + AddOptions(buffer, context); + + // TODO: add default command help + + return buffer.ToString(); + } + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandInitializer.cs b/CliFx/Services/ICommandInitializer.cs new file mode 100644 index 0000000..ed26b71 --- /dev/null +++ b/CliFx/Services/ICommandInitializer.cs @@ -0,0 +1,9 @@ +using CliFx.Models; + +namespace CliFx.Services +{ + public interface ICommandInitializer + { + ICommand InitializeCommand(CommandInput input); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandInputParser.cs b/CliFx/Services/ICommandInputParser.cs new file mode 100644 index 0000000..def430e --- /dev/null +++ b/CliFx/Services/ICommandInputParser.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using CliFx.Models; + +namespace CliFx.Services +{ + public interface ICommandInputParser + { + CommandInput ParseInput(IReadOnlyList commandLineArguments); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandOptionConverter.cs b/CliFx/Services/ICommandOptionConverter.cs deleted file mode 100644 index 125d17c..0000000 --- a/CliFx/Services/ICommandOptionConverter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using CliFx.Models; - -namespace CliFx.Services -{ - public interface ICommandOptionConverter - { - object ConvertOption(CommandOption option, Type targetType); - } -} \ No newline at end of file diff --git a/CliFx/Services/ICommandOptionInputConverter.cs b/CliFx/Services/ICommandOptionInputConverter.cs new file mode 100644 index 0000000..40b8c10 --- /dev/null +++ b/CliFx/Services/ICommandOptionInputConverter.cs @@ -0,0 +1,10 @@ +using System; +using CliFx.Models; + +namespace CliFx.Services +{ + public interface ICommandOptionInputConverter + { + object ConvertOption(CommandOptionInput option, Type targetType); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandOptionParser.cs b/CliFx/Services/ICommandOptionParser.cs deleted file mode 100644 index 7cd4c1f..0000000 --- a/CliFx/Services/ICommandOptionParser.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 983b848..0000000 --- a/CliFx/Services/ICommandResolver.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CliFx.Models; - -namespace CliFx.Services -{ - public interface ICommandResolver - { - ICommand ResolveCommand(CommandOptionSet optionSet); - } -} \ No newline at end of file diff --git a/CliFx/Services/ICommandSchemaResolver.cs b/CliFx/Services/ICommandSchemaResolver.cs new file mode 100644 index 0000000..576383b --- /dev/null +++ b/CliFx/Services/ICommandSchemaResolver.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using CliFx.Models; + +namespace CliFx.Services +{ + public interface ICommandSchemaResolver + { + IReadOnlyList ResolveAllSchemas(); + } +} \ No newline at end of file diff --git a/CliFx/Services/IConsoleWriter.cs b/CliFx/Services/IConsoleWriter.cs new file mode 100644 index 0000000..c361525 --- /dev/null +++ b/CliFx/Services/IConsoleWriter.cs @@ -0,0 +1,11 @@ +using CliFx.Models; + +namespace CliFx.Services +{ + public interface IConsoleWriter + { + void Write(TextSpan text); + + void WriteLine(TextSpan text); + } +} \ No newline at end of file diff --git a/CliFx/Services/IHelpTextBuilder.cs b/CliFx/Services/IHelpTextBuilder.cs new file mode 100644 index 0000000..4190981 --- /dev/null +++ b/CliFx/Services/IHelpTextBuilder.cs @@ -0,0 +1,9 @@ +using CliFx.Models; + +namespace CliFx.Services +{ + public interface IHelpTextBuilder + { + string Build(CommandContext context); + } +} \ No newline at end of file diff --git a/CliFx/Services/ITypeActivator.cs b/CliFx/Services/ITypeActivator.cs new file mode 100644 index 0000000..30236c5 --- /dev/null +++ b/CliFx/Services/ITypeActivator.cs @@ -0,0 +1,9 @@ +using System; + +namespace CliFx.Services +{ + public interface ITypeActivator + { + object Activate(Type type); + } +} \ No newline at end of file diff --git a/CliFx/Services/TypeActivator.cs b/CliFx/Services/TypeActivator.cs new file mode 100644 index 0000000..c2f1bf3 --- /dev/null +++ b/CliFx/Services/TypeActivator.cs @@ -0,0 +1,9 @@ +using System; + +namespace CliFx.Services +{ + public class TypeActivator : ITypeActivator + { + public object Activate(Type type) => Activator.CreateInstance(type); + } +} \ No newline at end of file