From d2599af90b722ed917a6b2dfc054a4d5e9994514 Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Thu, 25 Jul 2019 01:14:49 +0300 Subject: [PATCH] Rework architecture again --- CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj | 3 +- CliFx.Tests.Dummy/Commands/DefaultCommand.cs | 31 --- CliFx.Tests.Dummy/Commands/GreeterCommand.cs | 32 ++++ CliFx.Tests.Dummy/Commands/LogCommand.cs | 12 +- .../Commands/{AddCommand.cs => SumCommand.cs} | 12 +- CliFx.Tests/CliApplicationTests.cs | 181 ++++++++++++++++-- CliFx.Tests/CliFx.Tests.csproj | 2 +- CliFx.Tests/CommandFactoryTests.cs | 41 ++++ CliFx.Tests/CommandInitializerTests.cs | 55 +++--- CliFx.Tests/CommandSchemaResolverTests.cs | 45 ++--- CliFx.Tests/DummyTests.cs | 62 +++--- CliFx/Attributes/CommandAttribute.cs | 2 - CliFx/Attributes/CommandOptionAttribute.cs | 4 +- CliFx/CliApplication.cs | 120 +++++++++++- CliFx/CliFx.csproj | 2 +- CliFx/Command.cs | 59 ------ .../CannotConvertCommandOptionException.cs | 21 ++ CliFx/Exceptions/CommandErrorException.cs | 30 +++ .../CommandOptionConvertException.cs | 21 -- CliFx/Exceptions/CommandResolveException.cs | 21 -- .../MissingCommandOptionException.cs | 21 ++ CliFx/Extensions.cs | 9 - CliFx/ICommand.cs | 4 +- CliFx/Internal/Extensions.cs | 43 +---- CliFx/Models/CommandContext.cs | 18 +- CliFx/Models/CommandInput.cs | 2 +- CliFx/Models/CommandOptionInput.cs | 14 +- .../CommandOptionInputEqualityComparer.cs | 4 +- CliFx/Models/CommandOptionSchema.cs | 10 +- CliFx/Models/CommandSchema.cs | 5 +- CliFx/Models/CommandSchemaEqualityComparer.cs | 2 - CliFx/Models/ExitCode.cs | 26 --- CliFx/Models/Extensions.cs | 69 ++++++- CliFx/Services/CommandFactory.cs | 10 + CliFx/Services/CommandHelpTextBuilder.cs | 141 ++++++++++++++ CliFx/Services/CommandInitializer.cs | 96 ++-------- CliFx/Services/CommandOptionInputConverter.cs | 40 ++-- CliFx/Services/CommandSchemaResolver.cs | 76 +++----- CliFx/Services/Extensions.cs | 37 +++- CliFx/Services/HelpTextBuilder.cs | 106 ---------- CliFx/Services/ICommandFactory.cs | 10 + CliFx/Services/ICommandHelpTextBuilder.cs | 10 + CliFx/Services/ICommandInitializer.cs | 2 +- CliFx/Services/ICommandSchemaResolver.cs | 4 +- CliFx/Services/IHelpTextBuilder.cs | 9 - CliFx/Services/ITypeActivator.cs | 9 - CliFx/Services/TypeActivator.cs | 9 - Readme.md | 2 +- 48 files changed, 880 insertions(+), 664 deletions(-) delete mode 100644 CliFx.Tests.Dummy/Commands/DefaultCommand.cs create mode 100644 CliFx.Tests.Dummy/Commands/GreeterCommand.cs rename CliFx.Tests.Dummy/Commands/{AddCommand.cs => SumCommand.cs} (55%) create mode 100644 CliFx.Tests/CommandFactoryTests.cs delete mode 100644 CliFx/Command.cs create mode 100644 CliFx/Exceptions/CannotConvertCommandOptionException.cs create mode 100644 CliFx/Exceptions/CommandErrorException.cs delete mode 100644 CliFx/Exceptions/CommandOptionConvertException.cs delete mode 100644 CliFx/Exceptions/CommandResolveException.cs create mode 100644 CliFx/Exceptions/MissingCommandOptionException.cs delete mode 100644 CliFx/Extensions.cs delete mode 100644 CliFx/Models/ExitCode.cs create mode 100644 CliFx/Services/CommandFactory.cs create mode 100644 CliFx/Services/CommandHelpTextBuilder.cs delete mode 100644 CliFx/Services/HelpTextBuilder.cs create mode 100644 CliFx/Services/ICommandFactory.cs create mode 100644 CliFx/Services/ICommandHelpTextBuilder.cs delete mode 100644 CliFx/Services/IHelpTextBuilder.cs delete mode 100644 CliFx/Services/ITypeActivator.cs delete mode 100644 CliFx/Services/TypeActivator.cs diff --git a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj index dcae554..e71679a 100644 --- a/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj +++ b/CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj @@ -2,8 +2,9 @@ Exe - net45 + net46 latest + 1.2.3.4 diff --git a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs b/CliFx.Tests.Dummy/Commands/DefaultCommand.cs deleted file mode 100644 index 5648c02..0000000 --- a/CliFx.Tests.Dummy/Commands/DefaultCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text; -using CliFx.Attributes; -using CliFx.Models; -using CliFx.Services; - -namespace CliFx.Tests.Dummy.Commands -{ - [Command] - public class DefaultCommand : Command - { - [CommandOption("target", 't', Description = "Greeting target.")] - public string Target { get; set; } = "world"; - - [CommandOption('e', Description = "Whether the greeting should be enthusiastic.")] - public bool IsEnthusiastic { get; set; } - - protected override ExitCode Process() - { - var buffer = new StringBuilder(); - - buffer.Append("Hello ").Append(Target); - - if (IsEnthusiastic) - buffer.Append("!!!"); - - Output.WriteLine(buffer.ToString()); - - return ExitCode.Success; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/GreeterCommand.cs b/CliFx.Tests.Dummy/Commands/GreeterCommand.cs new file mode 100644 index 0000000..c1cf18b --- /dev/null +++ b/CliFx.Tests.Dummy/Commands/GreeterCommand.cs @@ -0,0 +1,32 @@ +using System.Text; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Models; +using CliFx.Services; + +namespace CliFx.Tests.Dummy.Commands +{ + [Command] + public class GreeterCommand : ICommand + { + [CommandOption("target", 't', Description = "Greeting target.")] + public string Target { get; set; } = "world"; + + [CommandOption('e', Description = "Whether the greeting should be exclaimed.")] + public bool IsExclaimed { get; set; } + + public Task ExecuteAsync(CommandContext context) + { + var buffer = new StringBuilder(); + + buffer.Append("Hello").Append(' ').Append(Target); + + if (IsExclaimed) + buffer.Append('!'); + + context.Output.WriteLine(buffer.ToString()); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs index b61e357..8e3a7ef 100644 --- a/CliFx.Tests.Dummy/Commands/LogCommand.cs +++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs @@ -1,13 +1,13 @@ using System; -using System.Globalization; +using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Models; using CliFx.Services; namespace CliFx.Tests.Dummy.Commands { - [Command("log", Description = "Calculate the logarithm of a value.")] - public class LogCommand : Command + [Command("log", Description = "Calculates the logarithm of a value.")] + public class LogCommand : ICommand { [CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")] public double Value { get; set; } @@ -15,12 +15,12 @@ namespace CliFx.Tests.Dummy.Commands [CommandOption("base", 'b', Description = "Logarithm base.")] public double Base { get; set; } = 10; - protected override ExitCode Process() + public Task ExecuteAsync(CommandContext context) { var result = Math.Log(Value, Base); - Output.WriteLine(result.ToString(CultureInfo.InvariantCulture)); + context.Output.WriteLine(result); - return ExitCode.Success; + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Commands/AddCommand.cs b/CliFx.Tests.Dummy/Commands/SumCommand.cs similarity index 55% rename from CliFx.Tests.Dummy/Commands/AddCommand.cs rename to CliFx.Tests.Dummy/Commands/SumCommand.cs index 06e6d26..dea684b 100644 --- a/CliFx.Tests.Dummy/Commands/AddCommand.cs +++ b/CliFx.Tests.Dummy/Commands/SumCommand.cs @@ -1,24 +1,24 @@ using System.Collections.Generic; -using System.Globalization; using System.Linq; +using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Models; using CliFx.Services; namespace CliFx.Tests.Dummy.Commands { - [Command("add", Description = "Calculate the sum of all input values.")] - public class AddCommand : Command + [Command("sum", Description = "Calculates the sum of all input values.")] + public class SumCommand : ICommand { [CommandOption("values", 'v', IsRequired = true, Description = "Input values.")] public IReadOnlyList Values { get; set; } - protected override ExitCode Process() + public Task ExecuteAsync(CommandContext context) { var result = Values.Sum(); - Output.WriteLine(result.ToString(CultureInfo.InvariantCulture)); + context.Output.WriteLine(result); - return ExitCode.Success; + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs index f0d5bee..fbbaa9c 100644 --- a/CliFx.Tests/CliApplicationTests.cs +++ b/CliFx.Tests/CliApplicationTests.cs @@ -1,7 +1,9 @@ -using System.Threading.Tasks; +using System; +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 @@ -9,32 +11,187 @@ namespace CliFx.Tests public partial class CliApplicationTests { [Command] - public class TestCommand : ICommand + private class TestDefaultCommand : ICommand { - public static ExitCode ExitCode { get; } = new ExitCode(13); + public Task ExecuteAsync(CommandContext context) => Task.CompletedTask; + } - public CommandContext Context { get; set; } + [Command("command")] + private class TestNamedCommand : ICommand + { + public Task ExecuteAsync(CommandContext context) => Task.CompletedTask; + } - public Task ExecuteAsync() => Task.FromResult(ExitCode); + [Command("faulty-command")] + private class FaultyCommand : ICommand + { + public Task ExecuteAsync(CommandContext context) => Task.FromException(new CommandErrorException(-1337)); } } [TestFixture] public partial class CliApplicationTests { + private static IEnumerable GetTestCases_RunAsync() + { + // Specified command is defined + + yield return new TestCaseData( + new[] {typeof(TestNamedCommand)}, + new[] {"command"} + ); + + yield return new TestCaseData( + new[] {typeof(TestNamedCommand)}, + new[] {"command", "--help"} + ); + + yield return new TestCaseData( + new[] {typeof(TestNamedCommand)}, + new[] {"command", "-h"} + ); + + yield return new TestCaseData( + new[] {typeof(TestNamedCommand)}, + new[] {"command", "-?"} + ); + + + // Default command is defined + + yield return new TestCaseData( + new[] {typeof(TestDefaultCommand)}, + new string[0] + ); + + yield return new TestCaseData( + new[] {typeof(TestDefaultCommand)}, + new[] {"--version"} + ); + + yield return new TestCaseData( + new[] {typeof(TestDefaultCommand)}, + new[] {"--help"} + ); + + yield return new TestCaseData( + new[] {typeof(TestDefaultCommand)}, + new[] {"-h"} + ); + + yield return new TestCaseData( + new[] {typeof(TestDefaultCommand)}, + new[] {"-?"} + ); + + // Default command is not defined + + yield return new TestCaseData( + new Type[0], + new string[0] + ); + + yield return new TestCaseData( + new Type[0], + new[] {"--version"} + ); + + yield return new TestCaseData( + new Type[0], + new[] {"--help"} + ); + + yield return new TestCaseData( + new Type[0], + new[] {"-h"} + ); + + yield return new TestCaseData( + new Type[0], + new[] {"-?"} + ); + + // Specified a faulty command + + yield return new TestCaseData( + new[] {typeof(FaultyCommand)}, + new[] {"--version"} + ); + + yield return new TestCaseData( + new[] {typeof(FaultyCommand)}, + new[] {"--help"} + ); + + yield return new TestCaseData( + new[] {typeof(FaultyCommand)}, + new[] {"-h"} + ); + + yield return new TestCaseData( + new[] {typeof(FaultyCommand)}, + new[] {"-?"} + ); + } + + private static IEnumerable GetTestCases_RunAsync_Negative() + { + // Specified command is not defined + + yield return new TestCaseData( + new Type[0], + new[] {"command"} + ); + + yield return new TestCaseData( + new Type[0], + new[] {"command", "--help"} + ); + + yield return new TestCaseData( + new Type[0], + new[] {"command", "-h"} + ); + + yield return new TestCaseData( + new Type[0], + new[] {"command", "-?"} + ); + + // Specified a faulty command + + yield return new TestCaseData( + new[] {typeof(FaultyCommand)}, + new[] {"faulty-command"} + ); + } + [Test] - public async Task RunAsync_Test() + [TestCaseSource(nameof(GetTestCases_RunAsync))] + public async Task RunAsync_Test(IReadOnlyList commandTypes, IReadOnlyList commandLineArguments) { // Arrange - var application = new CliApplication( - new CommandInputParser(), - new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)}))); + var application = new CliApplication(commandTypes); // Act - var exitCodeValue = await application.RunAsync(); + var exitCodeValue = await application.RunAsync(commandLineArguments); // Assert - Assert.That(exitCodeValue, Is.EqualTo(TestCommand.ExitCode.Value), "Exit code"); + Assert.That(exitCodeValue, Is.Zero, "Exit code"); + } + + [Test] + [TestCaseSource(nameof(GetTestCases_RunAsync_Negative))] + public async Task RunAsync_Negative_Test(IReadOnlyList commandTypes, IReadOnlyList commandLineArguments) + { + // Arrange + var application = new CliApplication(commandTypes); + + // Act + var exitCodeValue = await application.RunAsync(commandLineArguments); + + // Assert + Assert.That(exitCodeValue, Is.Not.Zero, "Exit code"); } } } \ No newline at end of file diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj index 11b59d6..811a4d4 100644 --- a/CliFx.Tests/CliFx.Tests.csproj +++ b/CliFx.Tests/CliFx.Tests.csproj @@ -1,7 +1,7 @@  - net45 + net46 false true true diff --git a/CliFx.Tests/CommandFactoryTests.cs b/CliFx.Tests/CommandFactoryTests.cs new file mode 100644 index 0000000..ada7fdc --- /dev/null +++ b/CliFx.Tests/CommandFactoryTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx.Models; +using CliFx.Services; +using NUnit.Framework; + +namespace CliFx.Tests +{ + public partial class CommandFactoryTests + { + private class TestCommand : ICommand + { + public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException(); + } + } + + [TestFixture] + public partial class CommandFactoryTests + { + private static IEnumerable GetTestCases_CreateCommand() + { + yield return new TestCaseData(typeof(TestCommand)); + } + + [Test] + [TestCaseSource(nameof(GetTestCases_CreateCommand))] + public void CreateCommand_Test(Type commandType) + { + // Arrange + var factory = new CommandFactory(); + + // Act + var schema = new CommandSchemaResolver().GetCommandSchema(commandType); + var command = factory.CreateCommand(schema); + + // Assert + Assert.That(command, Is.TypeOf(commandType)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/CommandInitializerTests.cs b/CliFx.Tests/CommandInitializerTests.cs index 83213a8..de61315 100644 --- a/CliFx.Tests/CommandInitializerTests.cs +++ b/CliFx.Tests/CommandInitializerTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Exceptions; @@ -10,7 +11,6 @@ namespace CliFx.Tests { public partial class CommandInitializerTests { - [Command] public class TestCommand : ICommand { [CommandOption("int", 'i', IsRequired = true)] @@ -22,9 +22,7 @@ namespace CliFx.Tests [CommandOption("bool", 'b', GroupName = "other-group")] public bool BoolOption { get; set; } - public CommandContext Context { get; set; } - - public Task ExecuteAsync() => throw new System.NotImplementedException(); + public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException(); } } @@ -94,26 +92,6 @@ namespace CliFx.Tests ); } - [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); @@ -126,15 +104,38 @@ namespace CliFx.Tests ); } + [Test] + [TestCaseSource(nameof(GetTestCases_InitializeCommand))] + public void InitializeCommand_Test(CommandInput commandInput, TestCommand expectedCommand) + { + // Arrange + var initializer = new CommandInitializer(); + + // Act + var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand)); + var command = new TestCommand(); + initializer.InitializeCommand(command, schema, commandInput); + + // Assert + Assert.Multiple(() => + { + 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)); + }); + } + [Test] [TestCaseSource(nameof(GetTestCases_InitializeCommand_IsRequired))] public void InitializeCommand_IsRequired_Test(CommandInput commandInput) { // Arrange - var initializer = new CommandInitializer(new CommandSchemaResolver(new[] {typeof(TestCommand)})); + var initializer = new CommandInitializer(); // Act & Assert - Assert.Throws(() => initializer.InitializeCommand(commandInput)); + var schema = new CommandSchemaResolver().GetCommandSchema(typeof(TestCommand)); + var command = new TestCommand(); + Assert.Throws(() => initializer.InitializeCommand(command, schema, commandInput)); } } } \ No newline at end of file diff --git a/CliFx.Tests/CommandSchemaResolverTests.cs b/CliFx.Tests/CommandSchemaResolverTests.cs index 2f2ab36..c519f16 100644 --- a/CliFx.Tests/CommandSchemaResolverTests.cs +++ b/CliFx.Tests/CommandSchemaResolverTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Models; @@ -10,8 +11,10 @@ namespace CliFx.Tests { public partial class CommandSchemaResolverTests { - [Command(Description = "Command description")] - public class TestCommand : ICommand + [Command("Command name", Description = "Command description")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")] + private class TestCommand : ICommand { [CommandOption("option-a", 'a', GroupName = "Group 1")] public int OptionA { get; set; } @@ -22,9 +25,7 @@ namespace CliFx.Tests [CommandOption("option-c", Description = "Option C description")] public bool OptionC { get; set; } - public CommandContext Context { get; set; } - - public Task ExecuteAsync() => throw new NotImplementedException(); + public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException(); } } @@ -34,36 +35,32 @@ namespace CliFx.Tests 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") - }) - } + typeof(TestCommand), + new CommandSchema(typeof(TestCommand), "Command name", "Command description", + new[] + { + new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionA)), + "option-a", 'a', "Group 1", false, null), + new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionB)), + "option-b", null, null, true, null), + new CommandOptionSchema(typeof(TestCommand).GetProperty(nameof(TestCommand.OptionC)), + "option-c", null, null, false, "Option C description") + }) ); } [Test] [TestCaseSource(nameof(GetTestCases_ResolveAllSchemas))] - public void ResolveAllSchemas_Test(IReadOnlyList sourceTypes, IReadOnlyList expectedSchemas) + public void GetCommandSchema_Test(Type commandType, CommandSchema expectedSchema) { // Arrange - var resolver = new CommandSchemaResolver(sourceTypes); + var resolver = new CommandSchemaResolver(); // Act - var schemas = resolver.ResolveAllSchemas(); + var schema = resolver.GetCommandSchema(commandType); // Assert - Assert.That(schemas, Is.EqualTo(expectedSchemas).Using(CommandSchemaEqualityComparer.Instance)); + Assert.That(schema, Is.EqualTo(expectedSchema).Using(CommandSchemaEqualityComparer.Instance)); } } } \ No newline at end of file diff --git a/CliFx.Tests/DummyTests.cs b/CliFx.Tests/DummyTests.cs index 7b334c1..fc18830 100644 --- a/CliFx.Tests/DummyTests.cs +++ b/CliFx.Tests/DummyTests.cs @@ -14,61 +14,59 @@ namespace CliFx.Tests [Test] [TestCase("", "Hello world")] [TestCase("-t .NET", "Hello .NET")] - [TestCase("-e", "Hello world!!!")] - [TestCase("add -v 1 2", "3")] - [TestCase("add -v 2.75 3.6 4.18", "10.53")] - [TestCase("add -v 4 -v 16", "20")] + [TestCase("-e", "Hello world!")] + [TestCase("sum -v 1 2", "3")] + [TestCase("sum -v 2.75 3.6 4.18", "10.53")] + [TestCase("sum -v 4 -v 16", "20")] + [TestCase("sum --values 2 5 --values 3", "10")] [TestCase("log -v 100", "2")] [TestCase("log --value 256 --base 2", "8")] public async Task CliApplication_RunAsync_Test(string arguments, string expectedOutput) { - // Act - var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); + // Arrange & Act + var result = await Cli.Wrap(DummyFilePath) + .SetArguments(arguments) + .EnableExitCodeValidation() + .EnableStandardErrorValidation() + .ExecuteAsync(); // Assert - 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"); - }); + Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput), "Stdout"); } [Test] [TestCase("--version")] - public async Task CliApplication_RunAsync_Version_Test(string arguments) + public async Task CliApplication_RunAsync_ShowVersion_Test(string arguments) { - // Act - var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); + // Arrange & Act + var result = await Cli.Wrap(DummyFilePath) + .SetArguments(arguments) + .EnableExitCodeValidation() + .EnableStandardErrorValidation() + .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"); - }); + Assert.That(result.StandardOutput.Trim(), Is.EqualTo(DummyVersionText), "Stdout"); } [Test] [TestCase("--help")] [TestCase("-h")] - [TestCase("add -h")] - [TestCase("add --help")] + [TestCase("sum -h")] + [TestCase("sum --help")] [TestCase("log -h")] [TestCase("log --help")] - public async Task CliApplication_RunAsync_Help_Test(string arguments) + public async Task CliApplication_RunAsync_ShowHelp_Test(string arguments) { - // Act - var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); + // Arrange & Act + var result = await Cli.Wrap(DummyFilePath) + .SetArguments(arguments) + .EnableExitCodeValidation() + .EnableStandardErrorValidation() + .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"); - }); + Assert.That(result.StandardOutput.Trim(), Is.Not.Empty, "Stdout"); } } } \ No newline at end of file diff --git a/CliFx/Attributes/CommandAttribute.cs b/CliFx/Attributes/CommandAttribute.cs index 5c88b58..ce77a7b 100644 --- a/CliFx/Attributes/CommandAttribute.cs +++ b/CliFx/Attributes/CommandAttribute.cs @@ -10,8 +10,6 @@ namespace CliFx.Attributes public string Description { get; set; } - public bool IsDefault => Name.IsNullOrWhiteSpace(); - public CommandAttribute(string name) { Name = name; diff --git a/CliFx/Attributes/CommandOptionAttribute.cs b/CliFx/Attributes/CommandOptionAttribute.cs index 36eea04..dc09f5e 100644 --- a/CliFx/Attributes/CommandOptionAttribute.cs +++ b/CliFx/Attributes/CommandOptionAttribute.cs @@ -9,10 +9,10 @@ namespace CliFx.Attributes public char? ShortName { get; } - public bool IsRequired { get; set; } - public string GroupName { get; set; } + public bool IsRequired { get; set; } + public string Description { get; set; } public CommandOptionAttribute(string name, char? shortName) diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 4fd04e4..4893442 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -1,33 +1,137 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Threading.Tasks; +using CliFx.Exceptions; +using CliFx.Internal; +using CliFx.Models; using CliFx.Services; namespace CliFx { - public class CliApplication : ICliApplication + public partial class CliApplication : ICliApplication { + private readonly IReadOnlyList _commandTypes; private readonly ICommandInputParser _commandInputParser; + private readonly ICommandSchemaResolver _commandSchemaResolver; + private readonly ICommandFactory _commandFactory; private readonly ICommandInitializer _commandInitializer; + private readonly ICommandHelpTextBuilder _commandHelpTextBuilder; - public CliApplication(ICommandInputParser commandInputParser, ICommandInitializer commandInitializer) + public CliApplication(IReadOnlyList commandTypes, + ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver, + ICommandFactory commandFactory, ICommandInitializer commandInitializer, ICommandHelpTextBuilder commandHelpTextBuilder) { + _commandTypes = commandTypes; _commandInputParser = commandInputParser; + _commandSchemaResolver = commandSchemaResolver; + _commandFactory = commandFactory; _commandInitializer = commandInitializer; + _commandHelpTextBuilder = commandHelpTextBuilder; + } + + public CliApplication(IReadOnlyList commandTypes) + : this(commandTypes, + new CommandInputParser(), new CommandSchemaResolver(), new CommandFactory(), + new CommandInitializer(), new CommandHelpTextBuilder()) + { } public CliApplication() - : this(new CommandInputParser(), new CommandInitializer()) + : this(GetDefaultCommandTypes()) { } public async Task RunAsync(IReadOnlyList commandLineArguments) { - var input = _commandInputParser.ParseInput(commandLineArguments); - var command = _commandInitializer.InitializeCommand(input); + var stdOut = ConsoleWriter.GetStandardOutput(); + var stdErr = ConsoleWriter.GetStandardError(); - var exitCode = await command.ExecuteAsync(); + try + { + var commandInput = _commandInputParser.ParseInput(commandLineArguments); - return exitCode.Value; + var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_commandTypes); + var matchingCommandSchema = availableCommandSchemas.FindByNameOrNull(commandInput.CommandName); + + // Fail if specified a command which is not defined + if (commandInput.IsCommandSpecified() && matchingCommandSchema == null) + { + stdErr.WriteLine($"Specified command [{commandInput.CommandName}] doesn't exist."); + return -1; + } + + // Show version if it was requested without specifying a command + if (commandInput.IsVersionRequested() && !commandInput.IsCommandSpecified()) + { + var versionText = Assembly.GetEntryAssembly()?.GetName().Version.ToString(); + stdOut.WriteLine(versionText); + return 0; + } + + // Use a stub if command was not specified but there is no default command defined + if (matchingCommandSchema == null) + { + matchingCommandSchema = _commandSchemaResolver.GetCommandSchema(typeof(StubDefaultCommand)); + } + + // Show help if it was requested + if (commandInput.IsHelpRequested()) + { + var helpText = _commandHelpTextBuilder.Build(availableCommandSchemas, matchingCommandSchema); + stdOut.WriteLine(helpText); + return 0; + } + + // Create an instance of the command + var command = matchingCommandSchema.Type == typeof(StubDefaultCommand) + ? new StubDefaultCommand(_commandHelpTextBuilder) + : _commandFactory.CreateCommand(matchingCommandSchema); + + // Populate command with options according to its schema + _commandInitializer.InitializeCommand(command, matchingCommandSchema, commandInput); + + // Create context and execute command + var commandContext = new CommandContext(commandInput, availableCommandSchemas, matchingCommandSchema, stdOut, stdErr); + await command.ExecuteAsync(commandContext); + + return 0; + } + catch (Exception ex) + { + stdErr.WriteLine(ex.ToString()); + return ex is CommandErrorException errorException ? errorException.ExitCode : -1; + } + finally + { + stdOut.Dispose(); + stdErr.Dispose(); + } + } + } + + public partial class CliApplication + { + private static IReadOnlyList GetDefaultCommandTypes() => + Assembly.GetEntryAssembly()?.ExportedTypes.Where(t => t.Implements(typeof(ICommand))).ToArray() ?? + Type.EmptyTypes; + + private sealed class StubDefaultCommand : ICommand + { + private readonly ICommandHelpTextBuilder _commandHelpTextBuilder; + + public StubDefaultCommand(ICommandHelpTextBuilder commandHelpTextBuilder) + { + _commandHelpTextBuilder = commandHelpTextBuilder; + } + + public Task ExecuteAsync(CommandContext context) + { + var helpText = _commandHelpTextBuilder.Build(context.AvailableCommandSchemas, context.MatchingCommandSchema); + context.Output.WriteLine(helpText); + return Task.CompletedTask; + } } } } \ No newline at end of file diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index deae8ad..1fc7a2e 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -1,7 +1,7 @@  - net45;netstandard2.0 + net46;netstandard2.0 latest 0.0.1 Tyrrrz diff --git a/CliFx/Command.cs b/CliFx/Command.cs deleted file mode 100644 index 4eedc9a..0000000 --- a/CliFx/Command.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 - { - [CommandOption("help", 'h', GroupName = "__help", Description = "Shows help.")] - public bool IsHelpRequested { get; set; } - - [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/Exceptions/CannotConvertCommandOptionException.cs b/CliFx/Exceptions/CannotConvertCommandOptionException.cs new file mode 100644 index 0000000..3b76cd7 --- /dev/null +++ b/CliFx/Exceptions/CannotConvertCommandOptionException.cs @@ -0,0 +1,21 @@ +using System; + +namespace CliFx.Exceptions +{ + public class CannotConvertCommandOptionException : Exception + { + public CannotConvertCommandOptionException() + { + } + + public CannotConvertCommandOptionException(string message) + : base(message) + { + } + + public CannotConvertCommandOptionException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/CliFx/Exceptions/CommandErrorException.cs b/CliFx/Exceptions/CommandErrorException.cs new file mode 100644 index 0000000..9fb89a6 --- /dev/null +++ b/CliFx/Exceptions/CommandErrorException.cs @@ -0,0 +1,30 @@ +using System; + +namespace CliFx.Exceptions +{ + public class CommandErrorException : Exception + { + public int ExitCode { get; } + + public CommandErrorException(int exitCode, string message, Exception innerException) + : base(message, innerException) + { + ExitCode = exitCode; + } + + public CommandErrorException(int exitCode, Exception innerException) + : this(exitCode, null, innerException) + { + } + + public CommandErrorException(int exitCode, string message) + : this(exitCode, message, null) + { + } + + public CommandErrorException(int exitCode) + : this(exitCode, null, null) + { + } + } +} \ No newline at end of file diff --git a/CliFx/Exceptions/CommandOptionConvertException.cs b/CliFx/Exceptions/CommandOptionConvertException.cs deleted file mode 100644 index cf4a0df..0000000 --- a/CliFx/Exceptions/CommandOptionConvertException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace CliFx.Exceptions -{ - public class CommandOptionConvertException : Exception - { - public CommandOptionConvertException() - { - } - - public CommandOptionConvertException(string message) - : base(message) - { - } - - public CommandOptionConvertException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} \ No newline at end of file diff --git a/CliFx/Exceptions/CommandResolveException.cs b/CliFx/Exceptions/CommandResolveException.cs deleted file mode 100644 index 86684f1..0000000 --- a/CliFx/Exceptions/CommandResolveException.cs +++ /dev/null @@ -1,21 +0,0 @@ -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/Exceptions/MissingCommandOptionException.cs b/CliFx/Exceptions/MissingCommandOptionException.cs new file mode 100644 index 0000000..e5c5a50 --- /dev/null +++ b/CliFx/Exceptions/MissingCommandOptionException.cs @@ -0,0 +1,21 @@ +using System; + +namespace CliFx.Exceptions +{ + public class MissingCommandOptionException : Exception + { + public MissingCommandOptionException() + { + } + + public MissingCommandOptionException(string message) + : base(message) + { + } + + public MissingCommandOptionException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/CliFx/Extensions.cs b/CliFx/Extensions.cs deleted file mode 100644 index 0ca26fc..0000000 --- a/CliFx/Extensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace CliFx -{ - public static class Extensions - { - public static Task RunAsync(this ICliApplication application) => application.RunAsync(new string[0]); - } -} \ No newline at end of file diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs index 7e21993..226826e 100644 --- a/CliFx/ICommand.cs +++ b/CliFx/ICommand.cs @@ -5,8 +5,6 @@ namespace CliFx { public interface ICommand { - CommandContext Context { get; set; } - - Task ExecuteAsync(); + Task ExecuteAsync(CommandContext context); } } \ No newline at end of file diff --git a/CliFx/Internal/Extensions.cs b/CliFx/Internal/Extensions.cs index 24afa0d..2a1ddbc 100644 --- a/CliFx/Internal/Extensions.cs +++ b/CliFx/Internal/Extensions.cs @@ -11,49 +11,12 @@ namespace CliFx.Internal public static string AsString(this char c) => new string(c, 1); - 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 string JoinToString(this IEnumerable source, string separator) => string.Join(separator, source); 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) - { - for (var currentType = type; currentType != null; currentType = currentType.BaseType) - { - if (currentType == baseType) - return true; - } - - return false; - } + public static IEnumerable ExceptNull(this IEnumerable source) where T : class => source.Where(i => i != null); public static bool IsEnumerable(this Type type) => type == typeof(IEnumerable) || type.GetInterfaces().Contains(typeof(IEnumerable)); @@ -79,5 +42,7 @@ namespace CliFx.Internal return array; } + + public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType); } } \ No newline at end of file diff --git a/CliFx/Models/CommandContext.cs b/CliFx/Models/CommandContext.cs index 7f06661..ff02725 100644 --- a/CliFx/Models/CommandContext.cs +++ b/CliFx/Models/CommandContext.cs @@ -1,17 +1,29 @@ using System.Collections.Generic; +using CliFx.Services; namespace CliFx.Models { public class CommandContext { + public CommandInput CommandInput { get; } + public IReadOnlyList AvailableCommandSchemas { get; } - public CommandSchema CommandSchema { get; } + public CommandSchema MatchingCommandSchema { get; } - public CommandContext(IReadOnlyList availableCommandSchemas, CommandSchema commandSchema) + public IConsoleWriter Output { get; } + + public IConsoleWriter Error { get; } + + public CommandContext(CommandInput commandInput, + IReadOnlyList availableCommandSchemas, CommandSchema matchingCommandSchema, + IConsoleWriter output, IConsoleWriter error) { + CommandInput = commandInput; AvailableCommandSchemas = availableCommandSchemas; - CommandSchema = commandSchema; + MatchingCommandSchema = matchingCommandSchema; + Output = output; + Error = error; } } } \ No newline at end of file diff --git a/CliFx/Models/CommandInput.cs b/CliFx/Models/CommandInput.cs index 72ca70a..05caa58 100644 --- a/CliFx/Models/CommandInput.cs +++ b/CliFx/Models/CommandInput.cs @@ -50,7 +50,7 @@ namespace CliFx.Models foreach (var option in Options) { - buffer.Append(option.Name); + buffer.Append(option.Alias); } buffer.Append(']'); diff --git a/CliFx/Models/CommandOptionInput.cs b/CliFx/Models/CommandOptionInput.cs index 19f1646..94bc6c3 100644 --- a/CliFx/Models/CommandOptionInput.cs +++ b/CliFx/Models/CommandOptionInput.cs @@ -4,23 +4,23 @@ namespace CliFx.Models { public class CommandOptionInput { - public string Name { get; } + public string Alias { get; } public IReadOnlyList Values { get; } - public CommandOptionInput(string name, IReadOnlyList values) + public CommandOptionInput(string alias, IReadOnlyList values) { - Name = name; + Alias = alias; Values = values; } - public CommandOptionInput(string name, string value) - : this(name, new[] {value}) + public CommandOptionInput(string alias, string value) + : this(alias, new[] {value}) { } - public CommandOptionInput(string name) - : this(name, new string[0]) + public CommandOptionInput(string alias) + : this(alias, new string[0]) { } } diff --git a/CliFx/Models/CommandOptionInputEqualityComparer.cs b/CliFx/Models/CommandOptionInputEqualityComparer.cs index 7b9d358..ae34f04 100644 --- a/CliFx/Models/CommandOptionInputEqualityComparer.cs +++ b/CliFx/Models/CommandOptionInputEqualityComparer.cs @@ -16,13 +16,13 @@ namespace CliFx.Models if (x is null || y is null) return false; - return StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name) && + return StringComparer.OrdinalIgnoreCase.Equals(x.Alias, y.Alias) && x.Values.SequenceEqual(y.Values, StringComparer.Ordinal); } /// public int GetHashCode(CommandOptionInput obj) => new HashCodeBuilder() - .Add(obj.Name, StringComparer.OrdinalIgnoreCase) + .Add(obj.Alias, StringComparer.OrdinalIgnoreCase) .AddMany(obj.Values, StringComparer.Ordinal) .Build(); } diff --git a/CliFx/Models/CommandOptionSchema.cs b/CliFx/Models/CommandOptionSchema.cs index 6e5af5f..37be7d6 100644 --- a/CliFx/Models/CommandOptionSchema.cs +++ b/CliFx/Models/CommandOptionSchema.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Reflection; -using CliFx.Internal; +using System.Reflection; namespace CliFx.Models { @@ -12,14 +10,14 @@ namespace CliFx.Models public char? ShortName { get; } - public bool IsRequired { get; } - public string GroupName { get; } + public bool IsRequired { get; } + public string Description { get; } public CommandOptionSchema(PropertyInfo property, string name, char? shortName, - bool isRequired, string groupName, string description) + string groupName, bool isRequired, string description) { Property = property; Name = name; diff --git a/CliFx/Models/CommandSchema.cs b/CliFx/Models/CommandSchema.cs index a4f8dea..e40deda 100644 --- a/CliFx/Models/CommandSchema.cs +++ b/CliFx/Models/CommandSchema.cs @@ -9,17 +9,14 @@ namespace CliFx.Models 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) + public CommandSchema(Type type, string name, string description, IReadOnlyList options) { Type = type; Name = name; - IsDefault = isDefault; Description = description; Options = options; } diff --git a/CliFx/Models/CommandSchemaEqualityComparer.cs b/CliFx/Models/CommandSchemaEqualityComparer.cs index 0e7d809..943beb6 100644 --- a/CliFx/Models/CommandSchemaEqualityComparer.cs +++ b/CliFx/Models/CommandSchemaEqualityComparer.cs @@ -18,7 +18,6 @@ namespace CliFx.Models 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); } @@ -27,7 +26,6 @@ namespace CliFx.Models 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(); diff --git a/CliFx/Models/ExitCode.cs b/CliFx/Models/ExitCode.cs deleted file mode 100644 index d730edb..0000000 --- a/CliFx/Models/ExitCode.cs +++ /dev/null @@ -1,26 +0,0 @@ -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/Models/Extensions.cs b/CliFx/Models/Extensions.cs index f12585f..6766d72 100644 --- a/CliFx/Models/Extensions.cs +++ b/CliFx/Models/Extensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using CliFx.Internal; @@ -6,16 +7,66 @@ namespace CliFx.Models { public static class Extensions { - 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)) - return true; + public static bool IsCommandSpecified(this CommandInput commandInput) => !commandInput.CommandName.IsNullOrWhiteSpace(); - if (shortName != null && o.Name.Length == 1 && o.Name.Single() == shortName) - return true; + public static bool IsEmpty(this CommandInput commandInput) => !commandInput.IsCommandSpecified() && !commandInput.Options.Any(); - return false; - }); + public static bool IsHelpOption(this CommandOptionInput optionInput) => + string.Equals(optionInput.Alias, "help", StringComparison.OrdinalIgnoreCase) || + string.Equals(optionInput.Alias, "h", StringComparison.OrdinalIgnoreCase) || + string.Equals(optionInput.Alias, "?", StringComparison.OrdinalIgnoreCase); + + public static bool IsVersionOption(this CommandOptionInput optionInput) => + string.Equals(optionInput.Alias, "version", StringComparison.OrdinalIgnoreCase); + + public static bool IsHelpRequested(this CommandInput commandInput) => commandInput.Options.Any(o => o.IsHelpOption()); + + public static bool IsVersionRequested(this CommandInput commandInput) => commandInput.Options.Any(o => o.IsVersionOption()); + + public static bool IsDefault(this CommandSchema commandSchema) => commandSchema.Name.IsNullOrWhiteSpace(); + + public static CommandSchema FindByNameOrNull(this IEnumerable commandSchemas, string name) => + commandSchemas.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)); + + public static IReadOnlyList FindSubCommandSchemas(this IEnumerable commandSchemas, + string parentName) + { + // For a command with no name, every other command is its subcommand + if (parentName.IsNullOrWhiteSpace()) + return commandSchemas.Where(c => !c.Name.IsNullOrWhiteSpace()).ToArray(); + + // For a named command, commands that are prefixed by its name are its subcommands + return commandSchemas.Where(c => !c.Name.IsNullOrWhiteSpace()) + .Where(c => c.Name.StartsWith(parentName + " ", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + public static CommandOptionSchema FindByAliasOrNull(this IEnumerable optionSchemas, string alias) => + optionSchemas.FirstOrDefault(o => o.GetAliases().Contains(alias, StringComparer.OrdinalIgnoreCase)); + + public static IReadOnlyList GetAliases(this CommandOptionSchema optionSchema) + { + var result = new List(); + + if (!optionSchema.Name.IsNullOrWhiteSpace()) + result.Add(optionSchema.Name); + + if (optionSchema.ShortName != null) + result.Add(optionSchema.ShortName.Value.AsString()); + + return result; + } + + public static IReadOnlyList GetAliasesWithPrefixes(this CommandOptionSchema optionSchema) + { + var result = new List(); + + if (!optionSchema.Name.IsNullOrWhiteSpace()) + result.Add("--" + optionSchema.Name); + + if (optionSchema.ShortName != null) + result.Add("-" + optionSchema.ShortName.Value.AsString()); + + return result; + } } } \ No newline at end of file diff --git a/CliFx/Services/CommandFactory.cs b/CliFx/Services/CommandFactory.cs new file mode 100644 index 0000000..ff8db23 --- /dev/null +++ b/CliFx/Services/CommandFactory.cs @@ -0,0 +1,10 @@ +using System; +using CliFx.Models; + +namespace CliFx.Services +{ + public class CommandFactory : ICommandFactory + { + public ICommand CreateCommand(CommandSchema schema) => (ICommand) Activator.CreateInstance(schema.Type); + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandHelpTextBuilder.cs b/CliFx/Services/CommandHelpTextBuilder.cs new file mode 100644 index 0000000..8c035c6 --- /dev/null +++ b/CliFx/Services/CommandHelpTextBuilder.cs @@ -0,0 +1,141 @@ +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 CommandHelpTextBuilder : ICommandHelpTextBuilder + { + // TODO: move to context? + private string GetExeName() => Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()?.Location); + + private void AddDescription(StringBuilder buffer, CommandSchema commands) + { + if (commands.Description.IsNullOrWhiteSpace()) + return; + + buffer.AppendLine("Description:"); + + buffer.Append(" "); + buffer.AppendLine(commands.Description); + + buffer.AppendLine(); + } + + private void AddUsage(StringBuilder buffer, CommandSchema command, IReadOnlyList subCommands) + { + buffer.AppendLine("Usage:"); + + buffer.Append(" "); + buffer.Append(GetExeName()); + + if (!command.Name.IsNullOrWhiteSpace()) + { + buffer.Append(' '); + buffer.Append(command.Name); + } + + if (subCommands.Any()) + { + buffer.Append(' '); + buffer.Append("[command]"); + } + + if (command.Options.Any()) + { + buffer.Append(' '); + buffer.Append("[options]"); + } + + buffer.AppendLine().AppendLine(); + } + + private void AddOptions(StringBuilder buffer, CommandSchema command) + { + if (!command.Options.Any()) + return; + + buffer.AppendLine("Options:"); + + foreach (var option in command.Options) + { + buffer.Append(option.IsRequired ? "* " : " "); + + buffer.Append(option.GetAliasesWithPrefixes().JoinToString("|")); + + if (!option.Description.IsNullOrWhiteSpace()) + { + buffer.Append(" "); + buffer.Append(option.Description); + } + + buffer.AppendLine(); + } + + // Help option + { + buffer.Append(" "); + buffer.Append("--help|-h"); + buffer.Append(" "); + buffer.Append("Shows helps text."); + buffer.AppendLine(); + } + + // Version option + if (command.IsDefault()) + { + buffer.Append(" "); + buffer.Append("--version"); + buffer.Append(" "); + buffer.Append("Shows application version."); + buffer.AppendLine(); + } + + buffer.AppendLine(); + } + + private void AddSubCommands(StringBuilder buffer, IReadOnlyList subCommands) + { + if (!subCommands.Any()) + return; + + buffer.AppendLine("Commands:"); + + foreach (var command in subCommands) + { + buffer.Append(" "); + + buffer.Append(command.Name); + + if (!command.Description.IsNullOrWhiteSpace()) + { + buffer.Append(" "); + buffer.Append(command.Description); + } + + buffer.AppendLine(); + } + + buffer.AppendLine(); + } + + public string Build(IReadOnlyList commandSchemas, CommandSchema commandSchema) + { + var buffer = new StringBuilder(); + + var subCommands = commandSchemas.FindSubCommandSchemas(commandSchema.Name); + + AddDescription(buffer, commandSchema); + AddUsage(buffer, commandSchema, subCommands); + AddOptions(buffer, commandSchema); + AddSubCommands(buffer, subCommands); + + return buffer.ToString().Trim(); + } + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandInitializer.cs b/CliFx/Services/CommandInitializer.cs index 002f67e..6bf5c52 100644 --- a/CliFx/Services/CommandInitializer.cs +++ b/CliFx/Services/CommandInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Internal; using CliFx.Models; @@ -10,115 +9,44 @@ 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) + public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter) { - _typeActivator = typeActivator; - _commandSchemaResolver = commandSchemaResolver; _commandOptionInputConverter = commandOptionInputConverter; } - public CommandInitializer(ICommandSchemaResolver commandSchemaResolver) - : this(new TypeActivator(), commandSchemaResolver, new CommandOptionInputConverter()) - { - } - public CommandInitializer() - : this(new CommandSchemaResolver()) + : this(new CommandOptionInputConverter()) { } - private CommandSchema GetDefaultSchema(IReadOnlyList schemas) + public void InitializeCommand(ICommand command, CommandSchema schema, CommandInput input) { - // 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)); + var optionSchema = schema.Options.FindByAliasOrNull(option.Alias); - if (optionInfo == null) + if (optionSchema == null) continue; - if (isGroupNameDetected && !string.Equals(groupName, optionInfo.GroupName, StringComparison.OrdinalIgnoreCase)) + if (isGroupNameDetected && !string.Equals(groupName, optionSchema.GroupName, StringComparison.OrdinalIgnoreCase)) continue; if (!isGroupNameDetected) { - groupName = optionInfo.GroupName; + groupName = optionSchema.GroupName; isGroupNameDetected = true; } - var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionInfo.Property.PropertyType); - optionInfo.Property.SetValue(command, convertedValue); + var convertedValue = _commandOptionInputConverter.ConvertOption(option, optionSchema.Property.PropertyType); + optionSchema.Property.SetValue(command, convertedValue); - properties.Add(optionInfo); + properties.Add(optionSchema); } var unsetRequiredOptions = schema.Options @@ -128,10 +56,8 @@ namespace CliFx.Services .ToArray(); if (unsetRequiredOptions.Any()) - throw new CommandResolveException( + throw new MissingCommandOptionException( $"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/CommandOptionInputConverter.cs b/CliFx/Services/CommandOptionInputConverter.cs index be8750b..75bafbc 100644 --- a/CliFx/Services/CommandOptionInputConverter.cs +++ b/CliFx/Services/CommandOptionInputConverter.cs @@ -39,7 +39,7 @@ namespace CliFx.Services if (bool.TryParse(value, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to boolean."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to boolean."); } // Char @@ -48,7 +48,7 @@ namespace CliFx.Services if (value.Length == 1) return value[0]; - throw new CommandOptionConvertException( + throw new CannotConvertCommandOptionException( $"Can't convert value [{value}] to char. The value is either empty or longer than one character."); } @@ -58,7 +58,7 @@ namespace CliFx.Services if (sbyte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to sbyte."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to sbyte."); } // Byte @@ -67,7 +67,7 @@ namespace CliFx.Services if (byte.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to byte."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to byte."); } // Short @@ -76,7 +76,7 @@ namespace CliFx.Services if (short.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to short."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to short."); } // Ushort @@ -85,7 +85,7 @@ namespace CliFx.Services if (ushort.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to ushort."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ushort."); } // Int @@ -94,7 +94,7 @@ namespace CliFx.Services if (int.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to int."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to int."); } // Uint @@ -103,7 +103,7 @@ namespace CliFx.Services if (uint.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to uint."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to uint."); } // Long @@ -112,7 +112,7 @@ namespace CliFx.Services if (long.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to long."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to long."); } // Ulong @@ -121,7 +121,7 @@ namespace CliFx.Services if (ulong.TryParse(value, NumberStyles.Integer, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to ulong."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to ulong."); } // Float @@ -130,7 +130,7 @@ namespace CliFx.Services if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to float."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to float."); } // Double @@ -139,7 +139,7 @@ namespace CliFx.Services if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to double."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to double."); } // Decimal @@ -148,7 +148,7 @@ namespace CliFx.Services if (decimal.TryParse(value, NumberStyles.Number, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to decimal."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to decimal."); } // DateTime @@ -157,7 +157,7 @@ namespace CliFx.Services if (DateTime.TryParse(value, _formatProvider, DateTimeStyles.None, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTime."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTime."); } // DateTimeOffset @@ -166,7 +166,7 @@ namespace CliFx.Services if (DateTimeOffset.TryParse(value, _formatProvider, DateTimeStyles.None, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to DateTimeOffset."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to DateTimeOffset."); } // TimeSpan @@ -175,7 +175,7 @@ namespace CliFx.Services if (TimeSpan.TryParse(value, _formatProvider, out var result)) return result; - throw new CommandOptionConvertException($"Can't convert value [{value}] to TimeSpan."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to TimeSpan."); } // Enum @@ -184,7 +184,7 @@ namespace CliFx.Services if (Enum.GetNames(targetType).Contains(value, StringComparer.OrdinalIgnoreCase)) return Enum.Parse(targetType, value, true); - throw new CommandOptionConvertException( + throw new CannotConvertCommandOptionException( $"Can't convert value [{value}] to [{targetType}]. The value is not defined on the enum."); } @@ -213,7 +213,7 @@ namespace CliFx.Services } // Unknown type - throw new CommandOptionConvertException($"Can't convert value [{value}] to unrecognized type [{targetType}]."); + throw new CannotConvertCommandOptionException($"Can't convert value [{value}] to unrecognized type [{targetType}]."); } // TODO: refactor this @@ -226,7 +226,7 @@ namespace CliFx.Services if (targetType.IsAssignableFrom(underlyingType.MakeArrayType())) return option.Values.Select(v => ConvertValue(v, underlyingType)).ToArray().ToNonGenericArray(underlyingType); - throw new CommandOptionConvertException( + throw new CannotConvertCommandOptionException( $"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}]."); } else if (option.Values.Count <= 1) @@ -239,7 +239,7 @@ namespace CliFx.Services else { // TODO: better exception - throw new CommandOptionConvertException( + throw new CannotConvertCommandOptionException( $"Can't convert sequence of values [{option.Values.JoinToString(", ")}] to type [{targetType}]."); } } diff --git a/CliFx/Services/CommandSchemaResolver.cs b/CliFx/Services/CommandSchemaResolver.cs index a801035..d2b4d59 100644 --- a/CliFx/Services/CommandSchemaResolver.cs +++ b/CliFx/Services/CommandSchemaResolver.cs @@ -1,74 +1,42 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; using CliFx.Attributes; +using CliFx.Internal; using CliFx.Models; namespace CliFx.Services { public class CommandSchemaResolver : ICommandSchemaResolver { - private readonly IReadOnlyList _sourceTypes; - - public CommandSchemaResolver(IReadOnlyList sourceTypes) + private CommandOptionSchema GetCommandOptionSchema(PropertyInfo optionProperty) { - _sourceTypes = sourceTypes; + var attribute = optionProperty.GetCustomAttribute(); + + if (attribute == null) + return null; + + return new CommandOptionSchema(optionProperty, + attribute.Name, + attribute.ShortName, + attribute.GroupName, + attribute.IsRequired, attribute.Description); } - public CommandSchemaResolver(IReadOnlyList sourceAssemblies) - : this(sourceAssemblies.SelectMany(a => a.ExportedTypes).ToArray()) + // TODO: validate stuff like duplicate names, multiple default commands, etc + public CommandSchema GetCommandSchema(Type commandType) { - } + if (!commandType.Implements(typeof(ICommand))) + throw new ArgumentException($"Command type must implement {nameof(ICommand)}.", nameof(commandType)); - public CommandSchemaResolver() - : this(new[] {Assembly.GetEntryAssembly()}) - { - } + var attribute = commandType.GetCustomAttribute(); - private IEnumerable GetCommandTypes() => _sourceTypes.Where(t => t.GetInterfaces().Contains(typeof(ICommand))); + var options = commandType.GetProperties().Select(GetCommandOptionSchema).ExceptNull().ToArray(); - 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; + return new CommandSchema(commandType, + attribute?.Name, + attribute?.Description, + options); } } } \ No newline at end of file diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs index 8969486..6eb45df 100644 --- a/CliFx/Services/Extensions.cs +++ b/CliFx/Services/Extensions.cs @@ -1,11 +1,42 @@ -using CliFx.Models; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +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 IReadOnlyList GetCommandSchemas(this ICommandSchemaResolver commandSchemaResolver, + IEnumerable commandTypes) => commandTypes.Select(commandSchemaResolver.GetCommandSchema).ToArray(); - public static void WriteLine(this IConsoleWriter consoleWriter, string text) => consoleWriter.WriteLine(new TextSpan(text)); + public static void Write(this IConsoleWriter consoleWriter, string text) => + consoleWriter.Write(new TextSpan(text)); + + public static void Write(this IConsoleWriter consoleWriter, IFormattable formattable) => + consoleWriter.Write(formattable.ToString(null, CultureInfo.InvariantCulture)); + + public static void Write(this IConsoleWriter consoleWriter, object obj) + { + if (obj is IFormattable formattable) + consoleWriter.Write(formattable); + else + consoleWriter.Write(obj.ToString()); + } + + public static void WriteLine(this IConsoleWriter consoleWriter, string text) => + consoleWriter.WriteLine(new TextSpan(text)); + + public static void WriteLine(this IConsoleWriter consoleWriter, IFormattable formattable) => + consoleWriter.WriteLine(formattable.ToString(null, CultureInfo.InvariantCulture)); + + public static void WriteLine(this IConsoleWriter consoleWriter, object obj) + { + if (obj is IFormattable formattable) + consoleWriter.WriteLine(formattable); + else + consoleWriter.WriteLine(obj.ToString()); + } } } \ No newline at end of file diff --git a/CliFx/Services/HelpTextBuilder.cs b/CliFx/Services/HelpTextBuilder.cs deleted file mode 100644 index 7fb5eb3..0000000 --- a/CliFx/Services/HelpTextBuilder.cs +++ /dev/null @@ -1,106 +0,0 @@ -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/ICommandFactory.cs b/CliFx/Services/ICommandFactory.cs new file mode 100644 index 0000000..313bde9 --- /dev/null +++ b/CliFx/Services/ICommandFactory.cs @@ -0,0 +1,10 @@ +using System; +using CliFx.Models; + +namespace CliFx.Services +{ + public interface ICommandFactory + { + ICommand CreateCommand(CommandSchema schema); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandHelpTextBuilder.cs b/CliFx/Services/ICommandHelpTextBuilder.cs new file mode 100644 index 0000000..c789bd7 --- /dev/null +++ b/CliFx/Services/ICommandHelpTextBuilder.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using CliFx.Models; + +namespace CliFx.Services +{ + public interface ICommandHelpTextBuilder + { + string Build(IReadOnlyList commandSchemas, CommandSchema commandSchema); + } +} \ No newline at end of file diff --git a/CliFx/Services/ICommandInitializer.cs b/CliFx/Services/ICommandInitializer.cs index ed26b71..07ee0cc 100644 --- a/CliFx/Services/ICommandInitializer.cs +++ b/CliFx/Services/ICommandInitializer.cs @@ -4,6 +4,6 @@ namespace CliFx.Services { public interface ICommandInitializer { - ICommand InitializeCommand(CommandInput input); + void InitializeCommand(ICommand command, CommandSchema schema, CommandInput input); } } \ No newline at end of file diff --git a/CliFx/Services/ICommandSchemaResolver.cs b/CliFx/Services/ICommandSchemaResolver.cs index 576383b..ed82aad 100644 --- a/CliFx/Services/ICommandSchemaResolver.cs +++ b/CliFx/Services/ICommandSchemaResolver.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System; using CliFx.Models; namespace CliFx.Services { public interface ICommandSchemaResolver { - IReadOnlyList ResolveAllSchemas(); + CommandSchema GetCommandSchema(Type commandType); } } \ No newline at end of file diff --git a/CliFx/Services/IHelpTextBuilder.cs b/CliFx/Services/IHelpTextBuilder.cs deleted file mode 100644 index 4190981..0000000 --- a/CliFx/Services/IHelpTextBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 30236c5..0000000 --- a/CliFx/Services/ITypeActivator.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index c2f1bf3..0000000 --- a/CliFx/Services/TypeActivator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace CliFx.Services -{ - public class TypeActivator : ITypeActivator - { - public object Activate(Type type) => Activator.CreateInstance(type); - } -} \ No newline at end of file diff --git a/Readme.md b/Readme.md index 0a1d91b..1ce1e26 100644 --- a/Readme.md +++ b/Readme.md @@ -18,7 +18,7 @@ CliFx is a powerful framework for building command line applications. ## Features - ...to be added with a stable release... -- Targets .NET Framework 4.5+ and .NET Standard 2.0+ +- Targets .NET Framework 4.6+ and .NET Standard 2.0+ - No external dependencies ## Usage