Skip to content

Commit

Permalink
Add the possibility to register multiple interceptors (#1412)
Browse files Browse the repository at this point in the history
Having the interceptors registered with the ITypeRegistrar also enables the usage of ITypeResolver in interceptors.
  • Loading branch information
nils-a authored Jan 6, 2024
1 parent e7ce6a6 commit a94bc15
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 7 deletions.
7 changes: 5 additions & 2 deletions docs/input/cli/commandApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
2 changes: 1 addition & 1 deletion src/Spectre.Console.Cli/ConfiguratorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ICommandInterceptor>(interceptor);
return configurator;
}

Expand Down
1 change: 1 addition & 0 deletions src/Spectre.Console.Cli/ICommandAppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public interface ICommandAppSettings
/// Gets or sets the <see cref="ICommandInterceptor"/> used
/// to intercept settings before it's being sent to the command.
/// </summary>
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
ICommandInterceptor? Interceptor { get; set; }

/// <summary>
Expand Down
22 changes: 21 additions & 1 deletion src/Spectre.Console.Cli/ICommandInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,25 @@ public interface ICommandInterceptor
/// </summary>
/// <param name="context">The intercepted <see cref="CommandContext"/>.</param>
/// <param name="settings">The intercepted <see cref="CommandSettings"/>.</param>
void Intercept(CommandContext context, CommandSettings settings);
void Intercept(CommandContext context, CommandSettings settings)
#if NETSTANDARD2_0
;
#else
{
}
#endif

/// <summary>
/// Intercepts a command result before it's passed as the result.
/// </summary>
/// <param name="context">The intercepted <see cref="CommandContext"/>.</param>
/// <param name="settings">The intercepted <see cref="CommandSettings"/>.</param>
/// <param name="result">The result from the command execution.</param>
void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
#if NETSTANDARD2_0
;
#else
{
}
#endif
}
24 changes: 21 additions & 3 deletions src/Spectre.Console.Cli/Internal/CommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private static string ResolveApplicationVersion(IConfiguration configuration)
VersionHelper.GetVersion(Assembly.GetEntryAssembly());
}

private static Task<int> Execute(
private static async Task<int> Execute(
CommandTree leaf,
CommandTree tree,
CommandContext context,
Expand All @@ -130,7 +130,19 @@ private static Task<int> 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<ICommandInterceptor>?)resolver.Resolve(typeof(IEnumerable<ICommandInterceptor>))
?? Array.Empty<ICommandInterceptor>()).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);
Expand All @@ -141,6 +153,12 @@ private static Task<int> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
7 changes: 7 additions & 0 deletions src/Spectre.Console.Testing/Cli/CallbackCommandInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,11 @@ public void Intercept(CommandContext context, CommandSettings settings)
{
_callback(context, settings);
}

#if NETSTANDARD2_0
/// <inheritdoc/>
public void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
{
}
#endif
}
90 changes: 90 additions & 0 deletions test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Interceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
namespace Spectre.Console.Tests.Unit.Cli;

public sealed partial class CommandAppTests
{
public sealed class Interceptor
{
public sealed class NoCommand : Command<NoCommand.Settings>
{
public sealed class Settings : CommandSettings
{
}

public override int Execute(CommandContext context, Settings settings)
{
return 0;
}
}

public sealed class MyInterceptor : ICommandInterceptor
{
private readonly Action<CommandContext, CommandSettings> _action;

public MyInterceptor(Action<CommandContext, CommandSettings> action)
{
_action = action;
}

public void Intercept(CommandContext context, CommandSettings settings)
{
_action(context, settings);
}
}

public sealed class MyResultInterceptor : ICommandInterceptor
{
private readonly Func<CommandContext, CommandSettings, int, int> _function;

public MyResultInterceptor(Func<CommandContext, CommandSettings, int, int> 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<NoCommand>();
var interceptor = new MyInterceptor((_, _) =>
{
count += 1;
});
app.Configure(config => config.SetInterceptor(interceptor));

// When
app.Run(Array.Empty<string>());

// 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<NoCommand>();
var interceptor = new MyResultInterceptor((_, _, _) =>
{
count += 1;
return Expected;
});
app.Configure(config => config.SetInterceptor(interceptor));

// When
var actual = app.Run(Array.Empty<string>());

// Then
count.ShouldBe(1);
actual.ShouldBe(Expected);
}
}
}

0 comments on commit a94bc15

Please sign in to comment.