Skip to content

Commit

Permalink
Add CommandExceptionHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
JKamsker committed Sep 25, 2023
1 parent 813a53c commit 8ddd694
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 13 deletions.
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

0 comments on commit 8ddd694

Please sign in to comment.