Skip to content

Commit

Permalink
Introduce CommandMethodForwardedToAttribute
Browse files Browse the repository at this point in the history
  • Loading branch information
mayuki committed May 28, 2020
1 parent 194ab87 commit 16109ed
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 12 deletions.
7 changes: 7 additions & 0 deletions Cocona.sln
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoconaSample.Advanced.Shell
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoconaSample.Advanced.OptionLikeCommand", "samples\Advanced.OptionLikeCommand\CoconaSample.Advanced.OptionLikeCommand.csproj", "{974EB617-441F-4DC8-88CE-92C896E0FC58}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoconaSample.Advanced.CommandMethodForwarding", "samples\Advanced.CommandMethodForwarding\CoconaSample.Advanced.CommandMethodForwarding.csproj", "{2E5BEBF1-FCDE-4DA3-9A06-4EC721E89AFD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -181,6 +183,10 @@ Global
{974EB617-441F-4DC8-88CE-92C896E0FC58}.Debug|Any CPU.Build.0 = Debug|Any CPU
{974EB617-441F-4DC8-88CE-92C896E0FC58}.Release|Any CPU.ActiveCfg = Release|Any CPU
{974EB617-441F-4DC8-88CE-92C896E0FC58}.Release|Any CPU.Build.0 = Release|Any CPU
{2E5BEBF1-FCDE-4DA3-9A06-4EC721E89AFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E5BEBF1-FCDE-4DA3-9A06-4EC721E89AFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E5BEBF1-FCDE-4DA3-9A06-4EC721E89AFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E5BEBF1-FCDE-4DA3-9A06-4EC721E89AFD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -209,6 +215,7 @@ Global
{E3BFD00E-199E-492F-813D-3D8B2A29258F} = {D154CC2E-568E-45D3-87EB-F0EDF70A763B}
{1951CD16-9CFD-4DF1-A0ED-7F4EC6AE4D72} = {26F0BA96-C75C-4BDF-A1CC-3A9F58A16D9E}
{974EB617-441F-4DC8-88CE-92C896E0FC58} = {26F0BA96-C75C-4BDF-A1CC-3A9F58A16D9E}
{2E5BEBF1-FCDE-4DA3-9A06-4EC721E89AFD} = {26F0BA96-C75C-4BDF-A1CC-3A9F58A16D9E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8DBA26EF-235B-4656-A5DD-00C266A42735}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Cocona.Core\Cocona.Core.csproj" />
<ProjectReference Include="..\..\src\Cocona\Cocona.csproj" />
</ItemGroup>

</Project>
24 changes: 24 additions & 0 deletions samples/Advanced.CommandMethodForwarding/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using Cocona;

namespace CoconaSample.Advanced.CommandMethodForwarding
{
class Program
{
static void Main(string[] args)
{
CoconaApp.Run<Program>(args);
}

[CommandMethodForwardedTo(typeof(Program), nameof(Program.Hey))]
public void Hello()
=> throw new NotImplementedException();

public void Hey([Argument]string name)
=> Console.WriteLine($"Hello {name}");

[CommandMethodForwardedTo(typeof(Cocona.Command.BuiltIn.BuiltInOptionLikeCommands), nameof(Cocona.Command.BuiltIn.BuiltInOptionLikeCommands.ShowHelp))]
public void MyHelp()
=> throw new NotSupportedException();
}
}
18 changes: 10 additions & 8 deletions src/Cocona.Core/Command/BuiltIn/BuiltInOptionLikeCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public class BuiltInOptionLikeCommands
null
), CommandOptionFlags.Hidden);

public static ValueTask<int> ShowHelp(
public ValueTask<int> ShowHelp(
[FromService]ICoconaAppContextAccessor appContext,
[FromService]ICoconaCommandProvider commandProvider,
[FromService]ICoconaCommandHelpProvider commandHelpProvider,
Expand All @@ -116,16 +116,18 @@ [FromService]ICoconaHelpRenderer helpRenderer
{
var feature = appContext.Current!.Features.Get<ICoconaCommandFeature>()!;
var commandCollection = feature.CommandCollection ?? commandProvider.GetCommandCollection(); // nested or root
var targetCommand = feature.CommandStack.Last();
var help = targetCommand.IsPrimaryCommand
? commandHelpProvider.CreateCommandsIndexHelp(commandCollection, feature.CommandStack.Take(feature.CommandStack.Count - 1).ToArray())
: commandHelpProvider.CreateCommandHelp(targetCommand, feature.CommandStack.Take(feature.CommandStack.Count - 1).ToArray());
var targetCommand = feature.CommandStack.LastOrDefault(); // When directly call the method, the CommandStack may be empty.
var help = targetCommand is null
? commandHelpProvider.CreateCommandsIndexHelp(commandCollection, Array.Empty<CommandDescriptor>())
: targetCommand.IsPrimaryCommand
? commandHelpProvider.CreateCommandsIndexHelp(commandCollection, feature.CommandStack.Take(feature.CommandStack.Count - 1).ToArray())
: commandHelpProvider.CreateCommandHelp(targetCommand, feature.CommandStack.Take(feature.CommandStack.Count - 1).ToArray());

console.Output.Write(helpRenderer.Render(help));
return new ValueTask<int>(129);
}

public static ValueTask<int> ShowVersion(
public ValueTask<int> ShowVersion(
[FromService]ICoconaCommandHelpProvider commandHelpProvider,
[FromService]ICoconaConsoleProvider console,
[FromService]ICoconaHelpRenderer helpRenderer
Expand All @@ -135,7 +137,7 @@ [FromService]ICoconaHelpRenderer helpRenderer
return new ValueTask<int>(0);
}

public static ValueTask<int> GenerateCompletionSource(
public ValueTask<int> GenerateCompletionSource(
[FromService]ICoconaConsoleProvider console,
[FromService]ICoconaCommandProvider commandProvider,
[FromService]ICoconaShellCompletionCodeProvider shellCompletionCodeProvider,
Expand All @@ -157,7 +159,7 @@ [Argument]string shellName
// If '--completion-candidates' option is provided, '--help' and '--version' options are also always provided.
// And these options prevent to perform unintended **destructive** action if the command doesn't support on-the-fly candidates feature.
// Fortunately, Cocona rejects unknown options by default. This options guard is fail-safe.
public static ValueTask<int> GetCompletionCandidates(
public ValueTask<int> GetCompletionCandidates(
[FromService]ICoconaConsoleProvider console,
[FromService]ICoconaShellCompletionCodeProvider shellCompletionCodeGenerator,
[FromService]ICoconaCompletionCandidates completionCandidates,
Expand Down
20 changes: 17 additions & 3 deletions src/Cocona.Core/Command/CoconaCommandProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ public CommandDescriptor CreateCommand(MethodInfo methodInfo, bool isSingleComma

// Collect Method attributes
var commandMethodDesc = GetCommandMethodDescriptor(methodInfo);

var commandAttr = commandMethodDesc.CommandAttribute;
var commandName = commandAttr?.Name ?? methodInfo.Name;
var description = commandAttr?.Description ?? string.Empty;
Expand All @@ -156,6 +155,15 @@ public CommandDescriptor CreateCommand(MethodInfo methodInfo, bool isSingleComma
var isHidden = commandMethodDesc.IsHidden;
var isIgnoreUnknownOptions = commandMethodDesc.IsIgnoreUnknownOptions;

// If the command method should forward to another command.
if (commandMethodDesc.CommandMethodForwardedTo is { } cmdForwardedTo)
{
var forwardTargetMethodInfo = cmdForwardedTo.CommandType.GetMethod(cmdForwardedTo.CommandMethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

methodInfo = forwardTargetMethodInfo
?? throw new InvalidOperationException($"The command '{methodInfo.Name}' is specified for command method forwarding. But the destination command '{cmdForwardedTo.CommandType.Name}.{cmdForwardedTo.CommandMethodName} was not found.");
}

var allOptions = new Dictionary<string, CommandOptionDescriptor>(StringComparer.OrdinalIgnoreCase);
var allOptionShortNames = new HashSet<char>();

Expand Down Expand Up @@ -345,6 +353,7 @@ private CommandMethodDescriptor GetCommandMethodDescriptor(MethodInfo methodInfo
var isPrimaryCommand = false;
var isIgnoreUnknownOptions = false;
var optionLikeCommands = new List<OptionLikeCommandAttribute>();
var commandMethodForwardedToAttr = default(CommandMethodForwardedToAttribute);

foreach (var attr in methodInfo.GetCustomAttributes(true))
{
Expand All @@ -365,12 +374,15 @@ private CommandMethodDescriptor GetCommandMethodDescriptor(MethodInfo methodInfo
case OptionLikeCommandAttribute optionLikeCommand:
optionLikeCommands.Add(optionLikeCommand);
break;
case CommandMethodForwardedToAttribute commandMethodForwardedTo:
commandMethodForwardedToAttr = commandMethodForwardedTo;
break;
}
}

isIgnoreUnknownOptions |= methodInfo.DeclaringType.GetCustomAttribute<IgnoreUnknownOptionsAttribute>() != null;

return new CommandMethodDescriptor(commandAttr, isHidden, isPrimaryCommand, isIgnoreUnknownOptions, optionLikeCommands);
return new CommandMethodDescriptor(commandAttr, isHidden, isPrimaryCommand, isIgnoreUnknownOptions, optionLikeCommands, commandMethodForwardedToAttr);
}

private readonly struct CommandMethodDescriptor
Expand All @@ -380,14 +392,16 @@ private readonly struct CommandMethodDescriptor
public bool IsPrimaryCommand { get; }
public bool IsIgnoreUnknownOptions { get; }
public IReadOnlyList<OptionLikeCommandAttribute> OptionLikeCommands { get; }
public CommandMethodForwardedToAttribute? CommandMethodForwardedTo { get; }

public CommandMethodDescriptor(CommandAttribute? commandAttr, bool isHidden, bool isPrimaryCommand, bool isIgnoreUnknownOptions, IReadOnlyList<OptionLikeCommandAttribute> optionLikeCommands)
public CommandMethodDescriptor(CommandAttribute? commandAttr, bool isHidden, bool isPrimaryCommand, bool isIgnoreUnknownOptions, IReadOnlyList<OptionLikeCommandAttribute> optionLikeCommands, CommandMethodForwardedToAttribute? commandMethodForwardedTo)
{
CommandAttribute = commandAttr;
IsHidden = isHidden;
IsPrimaryCommand = isPrimaryCommand;
IsIgnoreUnknownOptions = isIgnoreUnknownOptions;
OptionLikeCommands = optionLikeCommands;
CommandMethodForwardedTo = commandMethodForwardedTo;
}
}

Expand Down
22 changes: 22 additions & 0 deletions src/Cocona.Core/CommandMethodForwardedToAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Cocona
{
/// <summary>
/// Specifies that a method of the command is forwarded to another method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CommandMethodForwardedToAttribute : Attribute
{
public Type CommandType { get; }
public string CommandMethodName { get; }

public CommandMethodForwardedToAttribute(Type commandType, string commandMethodName)
{
CommandType = commandType ?? throw new ArgumentNullException(nameof(commandType));
CommandMethodName = commandMethodName ?? throw new ArgumentNullException(nameof(commandMethodName));
}
}
}
19 changes: 18 additions & 1 deletion test/Cocona.Test/Command/CommandProvider/CreateCommandTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ public void Hidden()
cmd.Flags.Should().HaveFlag(CommandFlags.Hidden);
}


[Fact]
public void HasOptionLikeCommands()
{
Expand All @@ -439,6 +438,16 @@ public void HasOptionLikeCommands()
cmd.OptionLikeCommands[2].Flags.Should().HaveFlag(CommandOptionFlags.Hidden);
}

[Fact]
public void CommandMethodForwardedTo()
{
var cmd = new CoconaCommandProvider(Array.Empty<Type>()).CreateCommand(GetMethod<CommandTest>(nameof(CommandTest.CommandMethodForwardedTo)), false, new Dictionary<string, List<(MethodInfo Method, CommandOverloadAttribute Attribute)>>());
cmd.Name.Should().Be(nameof(CommandTest.CommandMethodForwardedTo));
cmd.Description.Should().Be("CommandMethodForwardedTo-description");
cmd.Method.Should().BeSameAs(typeof(CommandTest_CommandMethodForwardedToTarget).GetMethod(nameof(CommandTest_CommandMethodForwardedToTarget.CommandMethodForwardedToTarget)));
cmd.Parameters.Should().HaveCount(1);
cmd.ReturnType.Should().Be<int>();
}

private static MethodInfo GetMethod<T>(string methodName)
{
Expand Down Expand Up @@ -483,6 +492,10 @@ public void IgnoreUnknownOptions() { }
[OptionLikeCommand("bar", new char[] { 'b' }, typeof(CommandTest), nameof(Default_NoOptions_NoArguments_NoReturn))]
[OptionLikeCommand("hidden", new char[] {}, typeof(CommandTest), nameof(Hidden))]
public void HasOptionLikeCommands() { }

[Command(Description = "CommandMethodForwardedTo-description")]
[CommandMethodForwardedTo(typeof(CommandTest_CommandMethodForwardedToTarget), nameof(CommandTest_CommandMethodForwardedToTarget.CommandMethodForwardedToTarget))]
public void CommandMethodForwardedTo() => throw new NotSupportedException();
}

[IgnoreUnknownOptions]
Expand All @@ -491,5 +504,9 @@ public class CommandTest_IgnoreUnknownOptions
public void IgnoreUnknownOptions() { }
}

public class CommandTest_CommandMethodForwardedToTarget
{
public int CommandMethodForwardedToTarget([Argument]string arg0) => 0;
}
}
}
32 changes: 32 additions & 0 deletions test/Cocona.Test/Integration/EndToEndTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using Cocona.Application;
using Cocona.Command.BuiltIn;
using Cocona.CommandLine;
using Cocona.ShellCompletion.Candidate;
using FluentAssertions;
Expand Down Expand Up @@ -448,5 +449,36 @@ public void Bye([Argument] string arg0)
}
}
}

[Fact]
public void CoconaApp_Run_CommandMethodForwarding_Multiple()
{
var (stdOut, stdErr, exitCode) = Run<TestCommand_CommandMethodForwarding_Multiple>(new string[] { "forward", "--option0", "OptionValue0", "ArgumentValue0" });

stdErr.Should().BeNullOrEmpty();
stdOut.Should().Contain("Forwarded:OptionValue0:ArgumentValue0");
}

[Fact]
public void CoconaApp_Run_CommandMethodForwarding_Multiple_BuiltInShowHelp()
{
var (stdOut, stdErr, exitCode) = Run<TestCommand_CommandMethodForwarding_Multiple>(new string[] { "my-help" });

stdErr.Should().BeNullOrEmpty();
stdOut.Should().Contain("Usage:");
stdOut.Should().Contain("Commands:");
}

class TestCommand_CommandMethodForwarding_Multiple
{
public void Hello() { }

[CommandMethodForwardedTo(typeof(TestCommand_CommandMethodForwarding_Multiple), nameof(TestCommand_CommandMethodForwarding_Multiple.ForwardTarget))]
public void Forward() { }
public void ForwardTarget(string option0, [Argument]string arg0) { Console.WriteLine($"Forwarded:{option0}:{arg0}"); }

[CommandMethodForwardedTo(typeof(BuiltInOptionLikeCommands), nameof(BuiltInOptionLikeCommands.ShowHelp))]
public void MyHelp() { }
}
}
}

0 comments on commit 16109ed

Please sign in to comment.