Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CommandExceptionHandler #1264

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/Spectre.Console.Cli/CommandExceptionArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Spectre.Console.Cli;
#pragma warning restore S2326

/// <summary>
/// Represents the args for a command exception.
/// </summary>
public readonly struct CommandExceptionArgs
{
/// <summary>
/// Gets the command context.
/// </summary>
public CommandContext Context { get; }

/// <summary>
/// Gets the exception that was thrown.
/// </summary>
public Exception Exception { get; }

/// <summary>
/// Gets the command type.
/// </summary>
public Type? CommandType { get; }

internal CommandExceptionArgs(CommandContext context, Exception exception, Type? commandType)
{
Context = context;
Exception = exception;
CommandType = commandType;
}
}
26 changes: 26 additions & 0 deletions src/Spectre.Console.Cli/ICommandExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Spectre.Console.Cli;

/// <summary>
/// Represents an args handler.
/// Exception handlers are used to handle exceptions that occur during command execution.
/// </summary>
public interface ICommandExceptionHandler
{
/// <summary>
/// Handles the specified args.
/// </summary>
/// <param name="args">The args to handle.</param>
/// <returns><c>true</c> if the args was handled, otherwise <c>false</c>.</returns>
bool Handle(CommandExceptionArgs args);
}

/// <summary>
/// Represents an args handler for a specific command.
/// </summary>
/// <typeparam name="TCommand">Type of the command.</typeparam>
// ReSharper disable once UnusedTypeParameter
#pragma warning disable S2326
public interface ICommandExceptionHandler<TCommand> : ICommandExceptionHandler
where TCommand : ICommand
{
}
75 changes: 75 additions & 0 deletions src/Spectre.Console.Cli/Internal/CommandExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Spectre.Console.Cli.Internal.Extensions;

namespace Spectre.Console.Cli.Internal;

internal static class CommandExceptionHandler
{
public static bool HandleException(
CommandTree leaf,
CommandContext context,
ITypeResolver resolver,
Exception ex)
{
var args = new CommandExceptionArgs(context, ex, leaf?.Command?.CommandType);

if (leaf?.Command?.CommandType == null)
{
return TryInvokeHandler(
resolver,
args,
typeof(IEnumerable<ICommandExceptionHandler>),
typeof(ICommandExceptionHandler));
}

var handlerType = typeof(ICommandExceptionHandler<>)
.MakeGenericType(leaf.Command.CommandType);

var enumerableHandlerType = typeof(IEnumerable<>)
.MakeGenericType(handlerType);

var handled = TryInvokeHandler(resolver, args, enumerableHandlerType, handlerType);
if (handled)
{
return true;
}

return TryInvokeHandler(resolver, args, typeof(IEnumerable<ICommandExceptionHandler>),
typeof(ICommandExceptionHandler));
}

private static bool TryInvokeHandler(
ITypeResolver resolver,
CommandExceptionArgs args,
Type enumerableHandlerType,
Type handlerType)
{
var handlers = resolver.TryResolve(enumerableHandlerType);
if (handlers is IEnumerable<ICommandExceptionHandler> exHandlerEnumerable)
{
foreach (var exHandlerItem in exHandlerEnumerable)
{
var isHandled = exHandlerItem.Handle(args);
if (isHandled)
{
return true;
}
}
}
else
{
var handler = resolver.TryResolve(handlerType);
if (handler is not ICommandExceptionHandler exHandler)
{
return false;
}

var isHandled = exHandler.Handle(args);
if (isHandled)
{
return true;
}
}

return false;
}
}
42 changes: 29 additions & 13 deletions src/Spectre.Console.Cli/Internal/CommandExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Spectre.Console.Cli.Internal;

namespace Spectre.Console.Cli;

internal sealed class CommandExecutor
Expand Down Expand Up @@ -121,26 +123,40 @@ 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,
ITypeResolver resolver,
IConfiguration configuration)
{
// Bind the command tree against the settings.
var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver);
configuration.Settings.Interceptor?.Intercept(context, settings);

// Create and validate the command.
var command = leaf.CreateCommand(resolver);
var validationResult = command.Validate(context, settings);
if (!validationResult.Successful)
try
{
throw CommandRuntimeException.ValidationFailed(validationResult);
// Bind the command tree against the settings.
var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver);
configuration.Settings.Interceptor?.Intercept(context, settings);

// Create and validate the command.
var command = leaf.CreateCommand(resolver);
var validationResult = command.Validate(context, settings);
if (!validationResult.Successful)
{
throw CommandRuntimeException.ValidationFailed(validationResult);
}

// Execute the command.
return await command.Execute(context, settings);
}
catch (Exception ex)
{
var handled = CommandExceptionHandler.HandleException(leaf, context, resolver, ex);
if (handled)
{
return -1;
}

// Execute the command.
return command.Execute(context, settings);
}
// Exception will be re-thrown and original stack trace will be preserved
throw;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Spectre.Console.Cli.Internal.Extensions;

internal static class TypeResolverExtensions
{
public static object? TryResolve(this ITypeResolver resolver, Type type)
{
if (resolver == null)
{
throw new ArgumentNullException(nameof(resolver));
}

if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

if (resolver is TypeResolverAdapter adapter)
{
return adapter.TryResolve(type);
}

try
{
return resolver.Resolve(type);
}
catch (Exception)
{
return null;
}
}
}
17 changes: 17 additions & 0 deletions src/Spectre.Console.Cli/Internal/TypeResolverAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ public TypeResolverAdapter(ITypeResolver? resolver)
}
}

public object? TryResolve(Type? type)
{
if (type == null)
{
throw new CommandRuntimeException("Cannot resolve null type.");
}

try
{
return _resolver?.Resolve(type);
}
catch (Exception)
{
return null;
}
}

public void Dispose()
{
if (_resolver is IDisposable disposable)
Expand Down
Loading