mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Rework architecture and implement auto help
This commit is contained in:
@@ -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<double> 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<ExitCode> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
140
CliFx.Tests/CommandInitializerTests.cs
Normal file
140
CliFx.Tests/CommandInitializerTests.cs
Normal file
@@ -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<ExitCode> ExecuteAsync() => throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public partial class CommandInitializerTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> 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<TestCaseData> 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<CommandResolveException>(() => initializer.InitializeCommand(commandInput));
|
||||
}
|
||||
}
|
||||
}
|
||||
170
CliFx.Tests/CommandInputParserTests.cs
Normal file
170
CliFx.Tests/CommandInputParserTests.cs
Normal file
@@ -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<TestCaseData> 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<string> commandLineArguments, CommandInput expectedCommandInput)
|
||||
{
|
||||
// Arrange
|
||||
var parser = new CommandInputParser();
|
||||
|
||||
// Act
|
||||
var commandInput = parser.ParseInput(commandLineArguments);
|
||||
|
||||
// Assert
|
||||
Assert.That(commandInput, Is.EqualTo(expectedCommandInput).Using(CommandInputEqualityComparer.Instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<TestCaseData> 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<string>),
|
||||
new[] {"value1", "value2"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new CommandOption("option", new[] {"value1", "value2"}),
|
||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
||||
typeof(IReadOnlyList<string>),
|
||||
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(() =>
|
||||
@@ -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<TestCaseData> 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<string> 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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<TestCaseData> 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<TestCaseData> 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<CommandResolveException>(() => resolver.ResolveCommand(commandOptionSet));
|
||||
}
|
||||
}
|
||||
}
|
||||
69
CliFx.Tests/CommandSchemaResolverTests.cs
Normal file
69
CliFx.Tests/CommandSchemaResolverTests.cs
Normal file
@@ -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<ExitCode> ExecuteAsync() => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public partial class CommandSchemaResolverTests
|
||||
{
|
||||
private static IEnumerable<TestCaseData> 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<Type> sourceTypes, IReadOnlyList<CommandSchema> expectedSchemas)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new CommandSchemaResolver(sourceTypes);
|
||||
|
||||
// Act
|
||||
var schemas = resolver.ResolveAllSchemas();
|
||||
|
||||
// Assert
|
||||
Assert.That(schemas, Is.EqualTo(expectedSchemas).Using(CommandSchemaEqualityComparer.Instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class DefaultCommandAttribute : CommandAttribute
|
||||
{
|
||||
public DefaultCommandAttribute()
|
||||
: base(null)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<int> RunAsync(IReadOnlyList<string> 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();
|
||||
|
||||
|
||||
@@ -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<ExitCode> 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<ExitCode> 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<ExitCode> ExecuteAsync()
|
||||
{
|
||||
if (IsHelpRequested)
|
||||
{
|
||||
ShowHelp();
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
|
||||
if (IsVersionRequested && Context.CommandSchema.IsDefault)
|
||||
{
|
||||
ShowVersion();
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
|
||||
return ProcessAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace CliFx
|
||||
{
|
||||
public interface ICommand
|
||||
{
|
||||
CommandContext Context { get; set; }
|
||||
|
||||
Task<ExitCode> ExecuteAsync();
|
||||
}
|
||||
}
|
||||
@@ -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<CommandOptionAttribute>();
|
||||
|
||||
return new CommandOptionProperty(property, attribute.Name, attribute.ShortName, attribute.IsRequired,
|
||||
attribute.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CommandOptionProperty> Options { get; }
|
||||
|
||||
public CommandType(Type type, string name, bool isDefault, string description, IReadOnlyList<CommandOptionProperty> 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<CommandAttribute>();
|
||||
|
||||
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<CommandType> GetCommandTypes(IEnumerable<Type> types) => types.Where(IsValid).Select(Initialize);
|
||||
}
|
||||
}
|
||||
38
CliFx/Internal/HashCodeBuilder.cs
Normal file
38
CliFx/Internal/HashCodeBuilder.cs
Normal file
@@ -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<int> hashCodes)
|
||||
{
|
||||
foreach (var hashCode in hashCodes)
|
||||
Add(hashCode);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public HashCodeBuilder Add<T>(T obj, IEqualityComparer<T> comparer) => Add(comparer.GetHashCode(obj));
|
||||
|
||||
public HashCodeBuilder Add<T>(T obj) => Add(obj, EqualityComparer<T>.Default);
|
||||
|
||||
public HashCodeBuilder AddMany<T>(IEnumerable<T> objs, IEqualityComparer<T> comparer) => Add(objs.Select(comparer.GetHashCode));
|
||||
|
||||
public HashCodeBuilder AddMany<T>(IEnumerable<T> objs) => AddMany(objs, EqualityComparer<T>.Default);
|
||||
|
||||
public int Build() => _code;
|
||||
}
|
||||
}
|
||||
17
CliFx/Models/CommandContext.cs
Normal file
17
CliFx/Models/CommandContext.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public class CommandContext
|
||||
{
|
||||
public IReadOnlyList<CommandSchema> AvailableCommandSchemas { get; }
|
||||
|
||||
public CommandSchema CommandSchema { get; }
|
||||
|
||||
public CommandContext(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema commandSchema)
|
||||
{
|
||||
AvailableCommandSchemas = availableCommandSchemas;
|
||||
CommandSchema = commandSchema;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
CliFx/Models/CommandInput.cs
Normal file
67
CliFx/Models/CommandInput.cs
Normal file
@@ -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<CommandOptionInput> Options { get; }
|
||||
|
||||
public CommandInput(string commandName, IReadOnlyList<CommandOptionInput> options)
|
||||
{
|
||||
CommandName = commandName;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public CommandInput(IReadOnlyList<CommandOptionInput> 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();
|
||||
}
|
||||
}
|
||||
34
CliFx/Models/CommandInputEqualityComparer.cs
Normal file
34
CliFx/Models/CommandInputEqualityComparer.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public partial class CommandInputEqualityComparer : IEqualityComparer<CommandInput>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,24 @@
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public class CommandOption
|
||||
public class CommandOptionInput
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public IReadOnlyList<string> Values { get; }
|
||||
|
||||
public CommandOption(string name, IReadOnlyList<string> values)
|
||||
public CommandOptionInput(string name, IReadOnlyList<string> 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])
|
||||
{
|
||||
}
|
||||
34
CliFx/Models/CommandOptionInputEqualityComparer.cs
Normal file
34
CliFx/Models/CommandOptionInputEqualityComparer.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public partial class CommandOptionInputEqualityComparer : IEqualityComparer<CommandOptionInput>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
}
|
||||
32
CliFx/Models/CommandOptionSchema.cs
Normal file
32
CliFx/Models/CommandOptionSchema.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
CliFx/Models/CommandOptionSchemaEqualityComparer.cs
Normal file
39
CliFx/Models/CommandOptionSchemaEqualityComparer.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public partial class CommandOptionSchemaEqualityComparer : IEqualityComparer<CommandOptionSchema>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<CommandOption> Options { get; }
|
||||
|
||||
public CommandOptionSet(string commandName, IReadOnlyList<CommandOption> options)
|
||||
{
|
||||
CommandName = commandName;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public CommandOptionSet(IReadOnlyList<CommandOption> 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]);
|
||||
}
|
||||
}
|
||||
27
CliFx/Models/CommandSchema.cs
Normal file
27
CliFx/Models/CommandSchema.cs
Normal file
@@ -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<CommandOptionSchema> Options { get; }
|
||||
|
||||
public CommandSchema(Type type, string name, bool isDefault, string description, IReadOnlyList<CommandOptionSchema> options)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
IsDefault = isDefault;
|
||||
Description = description;
|
||||
Options = options;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
CliFx/Models/CommandSchemaEqualityComparer.cs
Normal file
40
CliFx/Models/CommandSchemaEqualityComparer.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public partial class CommandSchemaEqualityComparer : IEqualityComparer<CommandSchema>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
24
CliFx/Models/TextSpan.cs
Normal file
24
CliFx/Models/TextSpan.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
137
CliFx/Services/CommandInitializer.cs
Normal file
137
CliFx/Services/CommandInitializer.cs
Normal file
@@ -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<CommandSchema> 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<CommandSchema> 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<CommandOptionSchema>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@ using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class CommandOptionParser : ICommandOptionParser
|
||||
public class CommandInputParser : ICommandInputParser
|
||||
{
|
||||
public CommandOptionSet ParseOptions(IReadOnlyList<string> commandLineArguments)
|
||||
// TODO: refactor
|
||||
public CommandInput ParseInput(IReadOnlyList<string> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
{
|
||||
@@ -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<Type> _availableTypes;
|
||||
private readonly ICommandOptionConverter _commandOptionConverter;
|
||||
|
||||
public CommandResolver(IReadOnlyList<Type> availableTypes, ICommandOptionConverter commandOptionConverter)
|
||||
{
|
||||
_availableTypes = availableTypes;
|
||||
_commandOptionConverter = commandOptionConverter;
|
||||
}
|
||||
|
||||
public CommandResolver(ICommandOptionConverter commandOptionConverter)
|
||||
: this(GetDefaultAvailableTypes(), commandOptionConverter)
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<CommandType> 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<Type> GetDefaultAvailableTypes() => Assembly.GetEntryAssembly()?.GetExportedTypes() ?? new Type[0];
|
||||
}
|
||||
}
|
||||
74
CliFx/Services/CommandSchemaResolver.cs
Normal file
74
CliFx/Services/CommandSchemaResolver.cs
Normal file
@@ -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<Type> _sourceTypes;
|
||||
|
||||
public CommandSchemaResolver(IReadOnlyList<Type> sourceTypes)
|
||||
{
|
||||
_sourceTypes = sourceTypes;
|
||||
}
|
||||
|
||||
public CommandSchemaResolver(IReadOnlyList<Assembly> sourceAssemblies)
|
||||
: this(sourceAssemblies.SelectMany(a => a.ExportedTypes).ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
public CommandSchemaResolver()
|
||||
: this(new[] {Assembly.GetEntryAssembly()})
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetCommandTypes() => _sourceTypes.Where(t => t.GetInterfaces().Contains(typeof(ICommand)));
|
||||
|
||||
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
|
||||
{
|
||||
var result = new List<CommandOptionSchema>();
|
||||
|
||||
foreach (var optionProperty in commandType.GetProperties())
|
||||
{
|
||||
var optionAttribute = optionProperty.GetCustomAttribute<CommandOptionAttribute>();
|
||||
|
||||
if (optionAttribute == null)
|
||||
continue;
|
||||
|
||||
result.Add(new CommandOptionSchema(optionProperty,
|
||||
optionAttribute.Name,
|
||||
optionAttribute.ShortName,
|
||||
optionAttribute.IsRequired,
|
||||
optionAttribute.GroupName,
|
||||
optionAttribute.Description));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IReadOnlyList<CommandSchema> ResolveAllSchemas()
|
||||
{
|
||||
var result = new List<CommandSchema>();
|
||||
|
||||
foreach (var commandType in GetCommandTypes())
|
||||
{
|
||||
var commandAttribute = commandType.GetCustomAttribute<CommandAttribute>();
|
||||
|
||||
if (commandAttribute == null)
|
||||
continue;
|
||||
|
||||
result.Add(new CommandSchema(commandType,
|
||||
commandAttribute.Name,
|
||||
commandAttribute.IsDefault,
|
||||
commandAttribute.Description,
|
||||
GetCommandOptionSchemas(commandType)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
CliFx/Services/ConsoleWriter.cs
Normal file
32
CliFx/Services/ConsoleWriter.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
11
CliFx/Services/Extensions.cs
Normal file
11
CliFx/Services/Extensions.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
106
CliFx/Services/HelpTextBuilder.cs
Normal file
106
CliFx/Services/HelpTextBuilder.cs
Normal file
@@ -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<string> GetOptionIdentifiers(CommandOptionSchema option)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
CliFx/Services/ICommandInitializer.cs
Normal file
9
CliFx/Services/ICommandInitializer.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandInitializer
|
||||
{
|
||||
ICommand InitializeCommand(CommandInput input);
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/ICommandInputParser.cs
Normal file
10
CliFx/Services/ICommandInputParser.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandInputParser
|
||||
{
|
||||
CommandInput ParseInput(IReadOnlyList<string> commandLineArguments);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandOptionConverter
|
||||
{
|
||||
object ConvertOption(CommandOption option, Type targetType);
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/ICommandOptionInputConverter.cs
Normal file
10
CliFx/Services/ICommandOptionInputConverter.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandOptionInputConverter
|
||||
{
|
||||
object ConvertOption(CommandOptionInput option, Type targetType);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandOptionParser
|
||||
{
|
||||
CommandOptionSet ParseOptions(IReadOnlyList<string> commandLineArguments);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandResolver
|
||||
{
|
||||
ICommand ResolveCommand(CommandOptionSet optionSet);
|
||||
}
|
||||
}
|
||||
10
CliFx/Services/ICommandSchemaResolver.cs
Normal file
10
CliFx/Services/ICommandSchemaResolver.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandSchemaResolver
|
||||
{
|
||||
IReadOnlyList<CommandSchema> ResolveAllSchemas();
|
||||
}
|
||||
}
|
||||
11
CliFx/Services/IConsoleWriter.cs
Normal file
11
CliFx/Services/IConsoleWriter.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface IConsoleWriter
|
||||
{
|
||||
void Write(TextSpan text);
|
||||
|
||||
void WriteLine(TextSpan text);
|
||||
}
|
||||
}
|
||||
9
CliFx/Services/IHelpTextBuilder.cs
Normal file
9
CliFx/Services/IHelpTextBuilder.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface IHelpTextBuilder
|
||||
{
|
||||
string Build(CommandContext context);
|
||||
}
|
||||
}
|
||||
9
CliFx/Services/ITypeActivator.cs
Normal file
9
CliFx/Services/ITypeActivator.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ITypeActivator
|
||||
{
|
||||
object Activate(Type type);
|
||||
}
|
||||
}
|
||||
9
CliFx/Services/TypeActivator.cs
Normal file
9
CliFx/Services/TypeActivator.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class TypeActivator : ITypeActivator
|
||||
{
|
||||
public object Activate(Type type) => Activator.CreateInstance(type);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user