Skip to content
Merged
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
26 changes: 19 additions & 7 deletions cloud-core/src/main/java/org/incendo/cloud/CommandTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
}

// This stores the argument value for this argument.
Object argumentValue = null;
ArgumentParseResult<?> argumentValue = null;

// Flag arguments need to be skipped over, so that further defaults are handled
if (commandInput.isEmpty() && !(child.component().type() == CommandComponent.ComponentType.FLAG)) {
Expand Down Expand Up @@ -489,7 +489,11 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {

final CompletableFuture<?> parseResult;
if (argumentValue != null) {
parseResult = CompletableFuture.completedFuture(argumentValue);
if (argumentValue.parsedValue().isPresent()) {
parseResult = CompletableFuture.completedFuture(argumentValue.parsedValue().get());
} else {
parseResult = CompletableFutures.failedFuture(this.argumentParseException(commandContext, child, argumentValue));
}
} else {
parseResult =
this.parseArgument(commandContext, child, commandInput, executor)
Expand Down Expand Up @@ -567,11 +571,7 @@ private boolean matchesLiteral(final @NonNull List<@NonNull CommandNode<C>> chil
if (result.failure().isPresent()) {
commandInput.cursor(currentInput.cursor());
resultFuture.completeExceptionally(
new ArgumentParseException(
result.failure().get(),
commandContext.sender(),
this.getComponentChain(node)
)
this.argumentParseException(commandContext, node, result)
);
} else {
resultFuture.complete(result);
Expand All @@ -580,6 +580,18 @@ private boolean matchesLiteral(final @NonNull List<@NonNull CommandNode<C>> chil
}, executor);
}

private @NonNull ArgumentParseException argumentParseException(
final CommandContext<C> commandContext,
final CommandNode<C> node,
final ArgumentParseResult<?> result
) {
return new ArgumentParseException(
result.failure().get(),
commandContext.sender(),
this.getComponentChain(node)
);
}

/**
* Returns suggestions from the input queue
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.parser.ArgumentParseResult;

import static java.util.Objects.requireNonNull;

/**
* Default value used when an optional argument is omitted by the command sender.
Expand All @@ -47,7 +50,21 @@ public interface DefaultValue<C, T> {
* @param <T> the argument type
*/
static <C, T> @NonNull DefaultValue<C, T> constant(final @NonNull T value) {
return new ConstantDefaultValue<>(value);
return new ConstantDefaultValue<>(requireNonNull(value, "value"));
}

/**
* Returns a default value that will be evaluated when the command is evaluated. The argument parser will be
* bypassed when using a dynamic default value.
*
* @param expression the expression producing the default value
* @return the default value instance
* @param <C> the command sender type
* @param <T> the argument type
*/
static <C, T> @NonNull DefaultValue<C, T> dynamic(final @NonNull DefaultValueProvider<C, T> expression) {
requireNonNull(expression, "expression");
return failableDynamic(ctx -> ArgumentParseResult.success(expression.evaluateDefault(ctx)));
}

/**
Expand All @@ -59,8 +76,8 @@ public interface DefaultValue<C, T> {
* @param <C> the command sender type
* @param <T> the argument type
*/
static <C, T> @NonNull DefaultValue<C, T> dynamic(final @NonNull DefaultValue<C, T> expression) {
return new DynamicDefaultValue<>(expression);
static <C, T> @NonNull DefaultValue<C, T> failableDynamic(final @NonNull DefaultValue<C, T> expression) {
return new DynamicDefaultValue<>(requireNonNull(expression, "expression"));
}

/**
Expand All @@ -81,19 +98,31 @@ public interface DefaultValue<C, T> {
* @param context the context
* @return the default value
*/
@NonNull T evaluateDefault(@NonNull CommandContext<C> context);
@NonNull ArgumentParseResult<T> evaluateDefault(@NonNull CommandContext<C> context);

@API(status = API.Status.STABLE)
@FunctionalInterface
interface DefaultValueProvider<C, T> {

/**
* Evaluates the default value for the given {@code context}.
*
* @param context the context
* @return the default value
*/
@NonNull T evaluateDefault(@NonNull CommandContext<C> context);
}

final class ConstantDefaultValue<C, T> implements DefaultValue<C, T> {

private final T value;
private final ArgumentParseResult<T> value;

private ConstantDefaultValue(final @NonNull T value) {
this.value = value;
this.value = ArgumentParseResult.success(value);
}

@Override
public @NonNull T evaluateDefault(final @NonNull CommandContext<C> context) {
public @NonNull ArgumentParseResult<T> evaluateDefault(final @NonNull CommandContext<C> context) {
return this.value;
}

Expand All @@ -106,7 +135,7 @@ public boolean equals(final Object object) {
return false;
}
final ConstantDefaultValue<?, ?> that = (ConstantDefaultValue<?, ?>) object;
return Objects.equals(this.value, that.value);
return Objects.equals(this.value.parsedValue().get(), that.value.parsedValue().get());
}

@Override
Expand All @@ -124,7 +153,7 @@ private DynamicDefaultValue(final @NonNull DefaultValue<C, T> defaultValue) {
}

@Override
public @NonNull T evaluateDefault(final @NonNull CommandContext<C> context) {
public @NonNull ArgumentParseResult<T> evaluateDefault(final @NonNull CommandContext<C> context) {
return this.defaultValue.evaluateDefault(context);
}

Expand Down Expand Up @@ -155,7 +184,7 @@ private ParsedDefaultValue(final @NonNull String string) {
}

@Override
public @NonNull T evaluateDefault(final @NonNull CommandContext<C> context) {
public @NonNull ArgumentParseResult<T> evaluateDefault(final @NonNull CommandContext<C> context) {
throw new UnsupportedOperationException();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@
//
package org.incendo.cloud.feature;

import com.google.common.truth.ThrowableSubject;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.TestCommandSender;
import org.incendo.cloud.component.DefaultValue;
import org.incendo.cloud.exception.ArgumentParseException;
import org.incendo.cloud.execution.CommandResult;
import org.incendo.cloud.key.CloudKey;
import org.incendo.cloud.parser.ArgumentParseResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static com.google.common.truth.Truth.assertThat;
import static org.incendo.cloud.parser.standard.IntegerParser.integerParser;
import static org.incendo.cloud.util.TestUtils.createManager;
import static org.junit.jupiter.api.Assertions.assertThrows;

class DefaultValueTest {

Expand Down Expand Up @@ -64,7 +69,8 @@ void Constant_HappyFlow_Success() throws Exception {
void Dynamic_HappyFlow_Success() throws Exception {
// Arrange
final CloudKey<Integer> key = CloudKey.of("int", Integer.class);
final DefaultValue<TestCommandSender, Integer> defaultValue = ctx -> ThreadLocalRandom.current().nextInt();
final DefaultValue<TestCommandSender, Integer> defaultValue =
DefaultValue.dynamic(ctx -> ThreadLocalRandom.current().nextInt());
this.commandManager.command(
this.commandManager.commandBuilder("test").optional(key, integerParser(), defaultValue)
);
Expand All @@ -76,6 +82,25 @@ void Dynamic_HappyFlow_Success() throws Exception {
assertThat(result.commandContext().get(key)).isNotNull();
}

@Test
void Dynamic_HappyFlow_Failure() {
// Arrange
final CloudKey<Integer> key = CloudKey.of("int", Integer.class);
final DefaultValue<TestCommandSender, Integer> defaultValue =
ctx -> ArgumentParseResult.failure(new RuntimeException("Oops this argument isn't really optional :D"));
this.commandManager.command(
this.commandManager.commandBuilder("test").optional(key, integerParser(), defaultValue)
);

// Act & Assert
final CompletionException completionException = assertThrows(CompletionException.class, () -> {
this.commandManager.commandExecutor().executeCommand(new TestCommandSender(), "test").join();
});
final ThrowableSubject argParse = assertThat(completionException).hasCauseThat();
argParse.isInstanceOf(ArgumentParseException.class);
argParse.hasCauseThat().isInstanceOf(RuntimeException.class);
}

@Test
void Parsed_HappyFlow_Success() throws Exception {
// Arrange
Expand Down