Skip to content

Commit

Permalink
feat: map output & exit codes
Browse files Browse the repository at this point in the history
  • Loading branch information
Citymonstret committed Dec 18, 2023
1 parent 6a8f59e commit 1699cba
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 30 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The example module contains a Spring Boot application with a couple of commands.
## features

- auto-discovery of `CommandBean` instances as well as `@ScanCommands`-annotated classes
- supports both interactive & non-interactive (CLI) commands
- support for Spring Shell features such as descriptions and command groups

![descriptions](img/descriptions.png)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// MIT License
//
// Copyright (c) 2023 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.cloud.spring;

import java.io.Serial;
import org.apiguardian.api.API;
import org.springframework.boot.ExitCodeGenerator;

/**
* Exception that indicates that a command execution failed.
*
* <p>This exception should not be handled, and it only exists so that we can map it to exit codes.</p>
*
* @since 1.0.0
*/
@API(status = API.Status.INTERNAL, consumers = "org.incendo.cloud.spring.*", since = "1.0.0")
public class FailureIndicationException extends RuntimeException implements ExitCodeGenerator {

@Serial
private static final long serialVersionUID = 3164725268923709954L;

@Override
public int getExitCode() {
return 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import cloud.commandframework.exceptions.InvalidSyntaxException;
import cloud.commandframework.exceptions.NoPermissionException;
import cloud.commandframework.exceptions.NoSuchCommandException;
import cloud.commandframework.execution.CommandResult;
import cloud.commandframework.keys.CloudKey;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -51,6 +52,7 @@
public class SpringCommandManager<C> extends CommandManager<C> implements CompletionResolver {

public static final CloudKey<String> COMMAND_GROUP_KEY = CloudKey.of("group", String.class);
public static final CloudKey<Object> OUTPUT = CloudKey.of("output", Object.class);

private static final String MESSAGE_INTERNAL_ERROR = "An internal error occurred while attempting to perform this command.";
private static final String MESSAGE_INVALID_SYNTAX = "Invalid Command Syntax. Correct command syntax is: ";
Expand Down Expand Up @@ -94,7 +96,13 @@ public final boolean hasPermission(final @NonNull C sender, final @NonNull Strin
@EventListener(CommandExecutionEvent.class)
void commandExecutionEvent(final @NonNull CommandExecutionEvent<C> event) {
final CommandInput commandInput = CommandInput.of(Arrays.asList(event.context().getRawArgs()));
this.executeCommand(this.commandSenderMapper.map(event.context()), commandInput.input());
try {
final CommandResult<C> result = this.executeCommand(this.commandSenderMapper.map(event.context()),
commandInput.input()).join();
event.result(result);
} catch (final Exception e) {
throw new FailureIndicationException();
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,25 @@
import cloud.commandframework.internal.CommandRegistrationHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.incendo.cloud.spring.event.CommandExecutionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandExceptionResolver;
import org.springframework.shell.command.CommandHandlingResult;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.stereotype.Component;

@Component
@API(status = API.Status.INTERNAL, consumers = "org.incendo.cloud.spring.*", since = "1.0.0")
public class SpringCommandRegistrationHandler<C> implements CommandRegistrationHandler<C>, ApplicationEventPublisherAware {
public class SpringCommandRegistrationHandler<C> implements CommandRegistrationHandler<C>, ApplicationEventPublisherAware,
CommandExceptionResolver {

private static final Logger LOGGER = LoggerFactory.getLogger(SpringCommandRegistrationHandler.class);

Expand Down Expand Up @@ -74,24 +79,18 @@ public final boolean registerCommand(final @NonNull Command<C> command) {
.description(command.commandDescription().description().textDescription())
.command(literals.toArray(new String[0]))
.withTarget()
.consumer(context -> {
this.applicationEventPublisher.publishEvent(new CommandExecutionEvent<>(command, context));
.function(context -> {
final CommandExecutionEvent<C> event = new CommandExecutionEvent<>(command, context);
this.applicationEventPublisher.publishEvent(event);
return Objects.requireNonNull(event.result(), "result").getCommandContext().getOrDefault(
SpringCommandManager.OUTPUT,
null
);
})
.and()/*
.withOption()
.defaultValue(null)
.type(String[].class)
.longNames("components")
.position(0)
.arity(CommandRegistration.OptionArity.ONE_OR_MORE)
.completion(context -> {
final CommandCompletionEvent<C> commandCompletionEvent = new CommandCompletionEvent<>(command, context);
this.applicationEventPublisher.publishEvent(commandCompletionEvent);
return commandCompletionEvent.suggestions().stream()
.map(suggestion -> new CompletionProposal(suggestion.suggestion()).complete(false).dontQuote(true))
.toList();
})
.and()*/
.and()
.withErrorHandling()
.resolver(this)
.and()
.group(command.commandMeta().getOrDefault(SpringCommandManager.COMMAND_GROUP_KEY, null))
.build();
this.commandCatalog.register(registration);
Expand All @@ -100,7 +99,15 @@ public final boolean registerCommand(final @NonNull Command<C> command) {
}

@Override
public void setApplicationEventPublisher(final @NonNull ApplicationEventPublisher applicationEventPublisher) {
public final void setApplicationEventPublisher(final @NonNull ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}

@Override
public final @Nullable CommandHandlingResult resolve(final @NonNull Exception exception) {
if (exception instanceof FailureIndicationException failure) {
return CommandHandlingResult.of("", 1);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
package org.incendo.cloud.spring.event;

import cloud.commandframework.Command;
import cloud.commandframework.execution.CommandResult;
import java.io.Serial;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.context.ApplicationEvent;
import org.springframework.shell.command.CommandContext;
Expand All @@ -44,6 +46,7 @@ public final class CommandExecutionEvent<C> extends ApplicationEvent {

private final Command<C> command;
private final CommandContext context;
private CommandResult<C> result;

/**
* Creates a new event.
Expand Down Expand Up @@ -75,6 +78,24 @@ public CommandExecutionEvent(final Command<C> command, final @NonNull CommandCon
return this.context;
}

/**
* Returns result of handling the command.
*
* @return the result, or {@code null} if the event has not been handled
*/
public @MonotonicNonNull CommandResult<C> result() {
return this.result;
}

/**
* Sets the result.
*
* @param result the result
*/
public void result(final @NonNull CommandResult<C> result) {
this.result = result;
}

@Override
public String toString() {
return String.format("CommandExecutionEvent{command=%s,context=%s}", this.command(), this.context());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
package org.incendo.cloud.spring.example;

import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.spring.SpringCommandManager;
import org.incendo.cloud.spring.SpringCommandSender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
Expand All @@ -39,15 +37,10 @@ public class ExampleApplication {

private static final Logger LOGGER = LoggerFactory.getLogger(ExampleApplication.class);

private SpringCommandManager<SpringCommandSender> springCommandManager;

/**
* Example application entrypoint.
*
* @param springCommandManager the command manager
*/
public ExampleApplication(final @NonNull SpringCommandManager<SpringCommandSender> springCommandManager) {
LOGGER.info("ExampleApplication woo");
public ExampleApplication() {
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ public void execute(final @NonNull CommandContext<SpringCommandSender> commandCo
final String name = commandContext.get("name");
final boolean override = commandContext.flags().hasFlag("override");
final Cat cat = this.catService.addCat(name, override);
commandContext.sender().writeLine(String.format("Added cat: %s", cat.name()));

// We can either write the output to the context:
commandContext.set(SpringCommandManager.OUTPUT, String.format("Added cat: %s", cat.name()));
// or print directly to the terminal:
// commandContext.sender().writeLine(String.format("Added cat: %s", cat.name()));
}
}
6 changes: 5 additions & 1 deletion example/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ spring:
application:
name: cloud-spring-example

main:
log-startup-info: false
banner-mode: off

logging:
level:
org.incendo: debug
org.incendo: info

0 comments on commit 1699cba

Please sign in to comment.