From a94bc1574632c802068fbe2c5788efadd6984ed5 Mon Sep 17 00:00:00 2001 From: Nils Andresen Date: Sat, 6 Jan 2024 23:28:20 +0100 Subject: [PATCH] Add the possibility to register multiple interceptors (#1412) Having the interceptors registered with the ITypeRegistrar also enables the usage of ITypeResolver in interceptors. --- docs/input/cli/commandApp.md | 7 +- .../ConfiguratorExtensions.cs | 2 +- .../ICommandAppSettings.cs | 1 + .../ICommandInterceptor.cs | 22 ++++- .../Internal/CommandExecutor.cs | 24 ++++- .../Configuration/CommandAppSettings.cs | 1 + .../Cli/CallbackCommandInterceptor.cs | 7 ++ .../Unit/CommandAppTests.Interceptor.cs | 90 +++++++++++++++++++ 8 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs diff --git a/docs/input/cli/commandApp.md b/docs/input/cli/commandApp.md index bef4be350..cff2bc952 100644 --- a/docs/input/cli/commandApp.md +++ b/docs/input/cli/commandApp.md @@ -81,9 +81,12 @@ Hint: If you do write your own implementation of `TypeRegistrar` and `TypeResolv there is a utility `TypeRegistrarBaseTests` available that can be used to ensure your implementations adhere to the required implementation. Simply call `TypeRegistrarBaseTests.RunAllTests()` and expect no `TypeRegistrarBaseTests.TestFailedException` to be thrown. ## Interception +Interceptors can be registered with the `TypeRegistrar` (or with a custom DI-Container). Alternatively, `CommandApp` also provides a `SetInterceptor` configuration. -`CommandApp` also provides a `SetInterceptor` configuration. An interceptor is run before all commands are executed. This is typically used for configuring logging or other infrastructure concerns. +All interceptors must implement `ICommandInterceptor`. Upon execution of a command, The `Intercept`-Method of an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings. +When the command has been run, the `InterceptResult`-Method of the same instance is called with the result of the command. +This provides an opportunity to modify the result and also to tear down any infrastructure in use. -All interceptors must implement `ICommandInterceptor`. Upon execution of a command, an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings. +The `Intercept`-Method of each interceptor is run before the command is executed and the `InterceptResult`-Method is run after it. These are typically used for configuring logging or other infrastructure concerns. For an example of using the interceptor to configure logging, see the [Serilog demo](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Logging). diff --git a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs index 6170164a7..985e208d3 100644 --- a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs +++ b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs @@ -229,7 +229,7 @@ public static IConfigurator SetInterceptor(this IConfigurator configurator, ICom throw new ArgumentNullException(nameof(configurator)); } - configurator.Settings.Interceptor = interceptor; + configurator.Settings.Registrar.RegisterInstance(interceptor); return configurator; } diff --git a/src/Spectre.Console.Cli/ICommandAppSettings.cs b/src/Spectre.Console.Cli/ICommandAppSettings.cs index 7455c8dde..c66478ce2 100644 --- a/src/Spectre.Console.Cli/ICommandAppSettings.cs +++ b/src/Spectre.Console.Cli/ICommandAppSettings.cs @@ -50,6 +50,7 @@ public interface ICommandAppSettings /// Gets or sets the used /// to intercept settings before it's being sent to the command. /// + [Obsolete("Register the interceptor with the ITypeRegistrar.")] ICommandInterceptor? Interceptor { get; set; } /// diff --git a/src/Spectre.Console.Cli/ICommandInterceptor.cs b/src/Spectre.Console.Cli/ICommandInterceptor.cs index 7854c2fbc..b2b963425 100644 --- a/src/Spectre.Console.Cli/ICommandInterceptor.cs +++ b/src/Spectre.Console.Cli/ICommandInterceptor.cs @@ -12,5 +12,25 @@ public interface ICommandInterceptor /// /// The intercepted . /// The intercepted . - void Intercept(CommandContext context, CommandSettings settings); + void Intercept(CommandContext context, CommandSettings settings) +#if NETSTANDARD2_0 + ; +#else + { + } +#endif + + /// + /// Intercepts a command result before it's passed as the result. + /// + /// The intercepted . + /// The intercepted . + /// The result from the command execution. + void InterceptResult(CommandContext context, CommandSettings settings, ref int result) +#if NETSTANDARD2_0 + ; +#else + { + } +#endif } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs index 1e844d817..1f57a66e1 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -121,7 +121,7 @@ private static string ResolveApplicationVersion(IConfiguration configuration) VersionHelper.GetVersion(Assembly.GetEntryAssembly()); } - private static Task Execute( + private static async Task Execute( CommandTree leaf, CommandTree tree, CommandContext context, @@ -130,7 +130,19 @@ private static Task Execute( { // Bind the command tree against the settings. var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver); - configuration.Settings.Interceptor?.Intercept(context, settings); + var interceptors = + ((IEnumerable?)resolver.Resolve(typeof(IEnumerable)) + ?? Array.Empty()).ToList(); +#pragma warning disable CS0618 // Type or member is obsolete + if (configuration.Settings.Interceptor != null) + { + interceptors.Add(configuration.Settings.Interceptor); + } +#pragma warning restore CS0618 // Type or member is obsolete + foreach (var interceptor in interceptors) + { + interceptor.Intercept(context, settings); + } // Create and validate the command. var command = leaf.CreateCommand(resolver); @@ -141,6 +153,12 @@ private static Task Execute( } // Execute the command. - return command.Execute(context, settings); + var result = await command.Execute(context, settings); + foreach (var interceptor in interceptors) + { + interceptor.InterceptResult(context, settings, ref result); + } + + return result; } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs index 815e1658b..f05c50c36 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs @@ -8,6 +8,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings public int MaximumIndirectExamples { get; set; } public bool ShowOptionDefaultValues { get; set; } public IAnsiConsole? Console { get; set; } + [Obsolete("Register the interceptor with the ITypeRegistrar.")] public ICommandInterceptor? Interceptor { get; set; } public ITypeRegistrarFrontend Registrar { get; set; } public CaseSensitivity CaseSensitivity { get; set; } diff --git a/src/Spectre.Console.Testing/Cli/CallbackCommandInterceptor.cs b/src/Spectre.Console.Testing/Cli/CallbackCommandInterceptor.cs index 4c71177d7..d2c610c42 100644 --- a/src/Spectre.Console.Testing/Cli/CallbackCommandInterceptor.cs +++ b/src/Spectre.Console.Testing/Cli/CallbackCommandInterceptor.cs @@ -21,4 +21,11 @@ public void Intercept(CommandContext context, CommandSettings settings) { _callback(context, settings); } + +#if NETSTANDARD2_0 + /// + public void InterceptResult(CommandContext context, CommandSettings settings, ref int result) + { + } +#endif } \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs new file mode 100644 index 000000000..338e2e294 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs @@ -0,0 +1,90 @@ +namespace Spectre.Console.Tests.Unit.Cli; + +public sealed partial class CommandAppTests +{ + public sealed class Interceptor + { + public sealed class NoCommand : Command + { + public sealed class Settings : CommandSettings + { + } + + public override int Execute(CommandContext context, Settings settings) + { + return 0; + } + } + + public sealed class MyInterceptor : ICommandInterceptor + { + private readonly Action _action; + + public MyInterceptor(Action action) + { + _action = action; + } + + public void Intercept(CommandContext context, CommandSettings settings) + { + _action(context, settings); + } + } + + public sealed class MyResultInterceptor : ICommandInterceptor + { + private readonly Func _function; + + public MyResultInterceptor(Func function) + { + _function = function; + } + + public void InterceptResult(CommandContext context, CommandSettings settings, ref int result) + { + result = _function(context, settings, result); + } + } + + [Fact] + public void Should_Run_The_Interceptor() + { + // Given + var count = 0; + var app = new CommandApp(); + var interceptor = new MyInterceptor((_, _) => + { + count += 1; + }); + app.Configure(config => config.SetInterceptor(interceptor)); + + // When + app.Run(Array.Empty()); + + // Then + count.ShouldBe(1); // to be sure + } + + [Fact] + public void Should_Run_The_ResultInterceptor() + { + // Given + var count = 0; + const int Expected = 123; + var app = new CommandApp(); + var interceptor = new MyResultInterceptor((_, _, _) => + { + count += 1; + return Expected; + }); + app.Configure(config => config.SetInterceptor(interceptor)); + + // When + var actual = app.Run(Array.Empty()); + + // Then + count.ShouldBe(1); + actual.ShouldBe(Expected); + } + } +} \ No newline at end of file