brigadier: Add support for wrapped parsers

This commit is contained in:
Zach Levis 2021-01-03 14:24:09 -08:00 committed by Jason
parent 79006ac40f
commit 62caa2d641
12 changed files with 889 additions and 1 deletions

View file

@ -2,4 +2,5 @@ dependencies {
implementation(project(":cloud-core"))
/* Needs to be provided by the platform */
compileOnly("com.mojang", "brigadier", Versions.brigadier)
testImplementation("com.mojang", "brigadier", Versions.brigadier)
}

View file

@ -39,6 +39,7 @@ import cloud.commandframework.arguments.standard.IntegerArgument;
import cloud.commandframework.arguments.standard.ShortArgument;
import cloud.commandframework.arguments.standard.StringArgument;
import cloud.commandframework.arguments.standard.StringArrayArgument;
import cloud.commandframework.brigadier.argument.WrappedBrigadierParser;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.permission.CommandPermission;
import cloud.commandframework.permission.Permission;
@ -92,6 +93,7 @@ public final class CloudBrigadierManager<C, S> {
private final Supplier<CommandContext<C>> dummyContextProvider;
private final CommandManager<C> commandManager;
private Function<S, C> brigadierCommandSenderMapper;
private Function<C, S> backwardsBrigadierCommandSenderMapper;
/**
* Create a new cloud brigadier manager
@ -108,6 +110,14 @@ public final class CloudBrigadierManager<C, S> {
this.commandManager = commandManager;
this.dummyContextProvider = dummyContextProvider;
this.registerInternalMappings();
commandManager.registerCommandPreProcessor(ctx -> {
if (this.backwardsBrigadierCommandSenderMapper != null) {
ctx.getCommandContext().store(
WrappedBrigadierParser.COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER,
this.backwardsBrigadierCommandSenderMapper.apply(ctx.getCommandContext().getSender())
);
}
});
}
private void registerInternalMappings() {
@ -195,6 +205,14 @@ public final class CloudBrigadierManager<C, S> {
/* Map String[] to a greedy string */
this.registerMapping(new TypeToken<StringArrayArgument.StringArrayParser<C>>() {
}, false, argument -> StringArgumentType.greedyString());
/* Map wrapped parsers to their native types */
this.registerWrapperMapping();
}
private <O> void registerWrapperMapping() {
/* a small hack to make type inference work properly... O doesn't behave as a wildcard */
this.registerMapping(new TypeToken<WrappedBrigadierParser<C, O>>() {
}, true, WrappedBrigadierParser::getNativeArgument);
}
/**
@ -219,6 +237,18 @@ public final class CloudBrigadierManager<C, S> {
return this.brigadierCommandSenderMapper;
}
/**
* Set the backwards mapper from Cloud to Brigadier command senders.
*
* <p>This is passed to completion requests for mapped argument types.</p>
*
* @param mapper the reverse brigadier sender mapper
* @since 1.4.0
*/
public void backwardsBrigadierSenderMapper(final @NonNull Function<@NonNull C, @Nullable S> mapper) {
this.backwardsBrigadierCommandSenderMapper = mapper;
}
/**
* Set whether to use Brigadier's native suggestions for number argument types.
* <p>

View file

@ -0,0 +1,71 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.brigadier.argument;
import com.mojang.brigadier.StringReader;
import java.util.Deque;
import java.util.Queue;
final class QueueAsStringReader extends StringReader {
private boolean closed;
private final Queue<String> input;
QueueAsStringReader(final Queue<String> input) {
super(String.join(" ", input));
this.input = input;
}
/**
* Update the underlying queue based on the reader state.
*
* <p>Can only be run once.</p>
*/
void updateQueue() {
if (this.closed) {
throw new IllegalStateException("double-closed");
}
this.closed = true;
/* Update elements in the queue to align it with the Brigadier cursor position */
int idx = this.getCursor();
while (idx > 0) {
final String next = this.input.element();
this.input.remove();
if (idx >= next.length()) {
idx -= next.length() + 1 /* whitespace */;
} else {
/* we've gotten to a partial word consumed by brigadier... let's try and modify the underlying queue */
if (!(this.input instanceof Deque<?>)) {
throw new IllegalArgumentException();
}
((Deque<String>) this.input).addFirst(next.substring(idx));
break;
}
}
}
}

View file

@ -0,0 +1,209 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.brigadier.argument;
import com.mojang.brigadier.StringReader;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Queue;
/**
* An interface combining {@link Queue} behaviour with a Brigadier {@link StringReader}.
*
* <p>This can be implemented either by wrapping an existing {@link StringReader} instance, or extending {@link StringReader}
* at its creation time to implement this interface.</p>
*/
public interface StringReaderAsQueue extends Queue<String> {
/**
* Given an existing Brigadier {@code StringReader}, get a view of it as a {@link Queue}
* @param reader the input reader
* @return a view of the contents of the reader as a {@link Queue} split by word.
*/
static StringReaderAsQueue from(final StringReader reader) {
if (reader instanceof StringReaderAsQueue) {
return (StringReaderAsQueue) reader;
} else {
return new StringReaderAsQueueImpl.Wrapping(reader);
}
}
/**
* Get the backing {@link StringReader} used to source data.
*
* @return the original reader
*/
StringReader getOriginal();
@Override
default boolean isEmpty() {
return !this.getOriginal().canRead();
}
@Override
default boolean contains(final Object element) {
if (element == null) {
return false;
}
// check if the string is in the collection, and
final int cursor = this.getOriginal().getCursor();
final String contents = this.getOriginal().getString();
final int idx = contents.indexOf((String) element, cursor);
if (idx == -1) {
return false;
}
final int length = this.getOriginal().getTotalLength();
final int end = idx + contents.length();
return (idx == cursor || Character.isWhitespace(contents.charAt(idx - 1)))
&& (end == length || Character.isWhitespace(contents.charAt(end)));
}
@Override
default @NonNull Iterator<String> iterator() {
// lazily break into words -- doesn't consume though!
return new Iterator<String>() {
private final String contents = StringReaderAsQueue.this.getOriginal().getString();
private int rangeStart = StringReaderAsQueue.this.getOriginal().getCursor();
private int rangeEnd = this.calculateNextEnd();
private int calculateNextEnd() {
if (this.rangeStart >= this.contents.length()) {
return -1;
}
final int nextSpace = StringReaderAsQueueImpl.nextWhitespace(this.contents, this.rangeStart);
return nextSpace == -1 ? this.contents.length() : nextSpace;
}
private void computeNext() {
this.rangeStart = this.rangeEnd + 1;
this.rangeEnd = this.calculateNextEnd();
}
@Override
public boolean hasNext() {
return this.rangeEnd > 0;
}
@Override
public String next() {
if (!this.hasNext()) {
throw new NoSuchElementException();
}
final String next = this.contents.substring(this.rangeStart, this.rangeEnd);
this.computeNext();
return next;
}
};
}
@Override
default Object[] toArray() {
if (this.isEmpty()) {
return new Object[0];
}
final ArrayList<String> out = new ArrayList<>(5);
for (final String element : this) {
/* addAll calls toArray on us... which would create a stack overflow */
//noinspection UseBulkOperation
out.add(element);
}
return out.toArray();
}
@Override
default <T> T[] toArray(final T[] a) {
if (this.isEmpty()) {
return Arrays.copyOf(a, 0);
}
final ArrayList<String> out = new ArrayList<>(5);
for (final String element : this) {
/* addAll calls toArray on us... which would create a stack overflow */
//noinspection UseBulkOperation
out.add(element);
}
return out.toArray(a);
}
@Override
default boolean add(final String element) {
throw new IllegalStateException("StringReaders cannot have elements appended");
}
@Override
default boolean offer(final String element) {
return false;
}
@Override
default String remove() {
final String result = this.poll();
if (result == null) {
throw new NoSuchElementException();
}
return result;
}
@Override
default String element() {
final String result = this.peek();
if (result == null) {
throw new NoSuchElementException();
}
return result;
}
@Override
default boolean containsAll(final @NonNull Collection<?> elements) {
throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud");
}
@Override
default boolean addAll(final @NonNull Collection<? extends String> elements) {
throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud");
}
@Override
default boolean removeAll(final @NonNull Collection<?> elements) {
throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud");
}
@Override
default boolean retainAll(final @NonNull Collection<?> elements) {
throw new UnsupportedOperationException("Complex Queue operations are not yet implemented in Cloud");
}
@Override
default void clear() { // consume all
this.getOriginal().setCursor(this.getOriginal().getTotalLength());
}
}

View file

@ -0,0 +1,136 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.brigadier.argument;
import com.mojang.brigadier.StringReader;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Objects;
import java.util.Queue;
/**
* A wrapper around Mojang's {@link StringReader} that implements the {@link Queue} interface.
*
* <p>This allows passing the full Brigadier state around through Cloud's parsing chain.</p>
*/
final class StringReaderAsQueueImpl {
private StringReaderAsQueueImpl() {
}
/* Next whitespace index starting at startIdx, or -1 if none is found */
static int nextWhitespace(final String input, final int startIdx) {
for (int i = startIdx, length = input.length(); i < length; ++i) {
if (Character.isWhitespace(input.charAt(i))) {
return i;
}
}
return -1;
}
/* Wrapping variant is implemented here
* Extending variant is only implementable with Mixin, because of clashing return types on the two interfaces (on `peek`). */
static final class Wrapping implements StringReaderAsQueue {
private final StringReader original;
private int nextSpaceIdx; /* the character before the start of a new word */
private @Nullable String nextWord;
Wrapping(final StringReader original) {
this.original = original;
this.nextSpaceIdx = original.getCursor() - 1;
this.advance();
}
@Override
public StringReader getOriginal() {
return this.original;
}
/* Brigadier doesn't automatically consume whitespace... in order to get the matched behaviour, we consume whitespace
* after every popped string.
*/
private void advance() {
final int startOfNextWord = this.nextSpaceIdx + 1;
this.nextSpaceIdx = nextWhitespace(this.original.getString(), startOfNextWord);
if (this.nextSpaceIdx != -1) {
this.nextWord = this.original.getString().substring(startOfNextWord, this.nextSpaceIdx);
} else if (startOfNextWord < this.original.getTotalLength()) {
this.nextWord = this.original.getString().substring(startOfNextWord);
this.nextSpaceIdx = this.original.getTotalLength() + 1;
} else {
this.nextWord = null;
}
this.original.setCursor(startOfNextWord);
}
@Override
public String poll() {
/* peek and then advance */
final String next = this.peek();
if (next != null) {
this.advance();
}
return next;
}
@Override
public String peek() {
return this.nextWord;
}
@Override
public int size() {
if (this.nextWord == null) {
return 0;
}
int counter = 1;
for (int i = this.nextSpaceIdx;
i != -1 && i < this.original.getTotalLength();
i = nextWhitespace(this.original.getString(), i + 1)) {
counter++;
}
return counter;
}
@Override
public boolean remove(final Object o) {
if (Objects.equals(o, this.nextWord)) {
this.advance();
return true;
}
return false;
}
@Override
public void clear() {
StringReaderAsQueue.super.clear();
this.nextWord = null;
this.nextSpaceIdx = -1;
}
}
}

View file

@ -0,0 +1,164 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.brigadier.argument;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.context.CommandContext;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import static java.util.Objects.requireNonNull;
/**
* A wrapped argument parser that can expose Brigadier argument types to the Cloud world.
*
* @param <C> the sender type
* @param <T> the value type of the argument
*/
public final class WrappedBrigadierParser<C, T> implements ArgumentParser<C, T> {
public static final String COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER = "_cloud_brigadier_native_sender";
private final ArgumentType<T> nativeType;
private final int expectedArgumentCount;
/**
* Create an argument parser based on a brigadier command.
*
* @param nativeType the native command type
*/
public WrappedBrigadierParser(final ArgumentType<T> nativeType) {
this(nativeType, DEFAULT_ARGUMENT_COUNT);
}
/**
* Create an argument parser based on a brigadier command.
*
* @param nativeType the native command type
* @param expectedArgumentCount the number of arguments the brigadier type is expected to consume
*/
public WrappedBrigadierParser(
final ArgumentType<T> nativeType,
final int expectedArgumentCount
) {
this.nativeType = requireNonNull(nativeType, "brigadierType");
this.expectedArgumentCount = expectedArgumentCount;
}
/**
* Get the backing Brigadier {@link ArgumentType} for this parser.
*
* @return the argument type
*/
public ArgumentType<T> getNativeArgument() {
return this.nativeType;
}
@Override
public @NonNull ArgumentParseResult<@NonNull T> parse(
@NonNull final CommandContext<@NonNull C> commandContext,
@NonNull final Queue<@NonNull String> inputQueue
) {
// Convert to a brig reader
final StringReader reader;
if (inputQueue instanceof StringReader) {
reader = (StringReader) inputQueue;
} else if (inputQueue instanceof StringReaderAsQueue) {
reader = ((StringReaderAsQueue) inputQueue).getOriginal();
} else {
reader = new QueueAsStringReader(inputQueue);
}
// Then try to parse
try {
return ArgumentParseResult.success(this.nativeType.parse(reader));
} catch (final CommandSyntaxException ex) {
return ArgumentParseResult.failure(ex);
} finally {
if (reader instanceof QueueAsStringReader) {
((QueueAsStringReader) reader).updateQueue();
}
}
}
@Override
public @NonNull List<@NonNull String> suggestions(
final @NonNull CommandContext<C> commandContext,
final @NonNull String input
) {
/*
* Strictly, this is incorrect.
* However, it seems that all Mojang really does with the context passed here
* is use it to query data on the native sender. Hopefully this hack holds up.
*/
final com.mojang.brigadier.context.CommandContext<Object> reverseMappedContext = new com.mojang.brigadier.context.CommandContext<>(
commandContext.getOrDefault(COMMAND_CONTEXT_BRIGADIER_NATIVE_SENDER, commandContext.getSender()),
commandContext.getRawInputJoined(),
Collections.emptyMap(),
null,
null,
Collections.emptyList(),
StringRange.at(0),
null,
null,
false
);
final CompletableFuture<Suggestions> result = this.nativeType.listSuggestions(reverseMappedContext,
new SuggestionsBuilder(input, 0));
/* again, avert your eyes */
final List<Suggestion> suggestions = result.join().getList();
final List<String> out = new ArrayList<>(suggestions.size());
for (final Suggestion suggestion : suggestions) {
out.add(suggestion.getText());
}
return out;
}
@Override
public boolean isContextFree() {
return true;
}
@Override
public int getRequestedArgumentCount() {
return this.expectedArgumentCount;
}
}

View file

@ -0,0 +1,29 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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.
//
/**
* Support for wrapping brigadier {@link com.mojang.brigadier.arguments.ArgumentType ArgumentTypes}
* as Cloud {@link cloud.commandframework.arguments.parser.ArgumentParser}.
*/
package cloud.commandframework.brigadier.argument;

View file

@ -23,6 +23,14 @@
//
/**
* Brigadier mappings
* Brigadier mappings.
*
* <p>For platform implementations using Brigadier, {@link cloud.commandframework.brigadier.CloudBrigadierManager} can map
* Cloud {@link cloud.commandframework.CommandTree command trees} to Brigadier nodes.</p>
*
* <p>To bridge Brigadier and Cloud argument types, an argument parser that wraps Brigadier argument types is available in
* {@link cloud.commandframework.brigadier.argument.WrappedBrigadierParser}. Other classes in that package allow constructing
* Brigadier {@link com.mojang.brigadier.StringReader} instances that can be used for efficient interoperability with
* Brigadier.</p>
*/
package cloud.commandframework.brigadier;

View file

@ -0,0 +1,76 @@
package cloud.commandframework.brigadier.argument;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class QueueAsStringReaderTest {
private static Queue<String> words(final String... elements) {
return new LinkedList<>(Arrays.asList(elements));
}
@Test
void testUnchanged() {
final Queue<String> contents = words("hello", "world");
final QueueAsStringReader reader = new QueueAsStringReader(contents);
reader.updateQueue();
assertEquals(words("hello", "world"), contents);
}
@Test
void testSingleWordRemoved() throws CommandSyntaxException {
final Queue<String> contents = words("hello", "some", "worlds");
final QueueAsStringReader reader = new QueueAsStringReader(contents);
assertEquals("hello", reader.readString());
reader.updateQueue();
assertEquals(words("some", "worlds"), contents);
}
@Test
void testBeginningAndMiddleRemoved() throws CommandSyntaxException {
final Queue<String> contents = words("hello", "some", "worlds");
final QueueAsStringReader reader = new QueueAsStringReader(contents);
assertEquals("hello", reader.readString());
reader.skipWhitespace();
assertEquals("some", reader.readString());
reader.updateQueue();
assertEquals(words("worlds"), contents);
}
@Test
void testAllThreeWordsRead() throws CommandSyntaxException {
final Queue<String> contents = words("hello", "some", "worlds");
final QueueAsStringReader reader = new QueueAsStringReader(contents);
assertEquals("hello", reader.readString());
reader.skipWhitespace();
assertEquals("some", reader.readString());
reader.skipWhitespace();
assertEquals("worlds", reader.readString());
reader.updateQueue();
assertTrue(contents.isEmpty());
}
@Test
void testPartialWordRead() throws CommandSyntaxException {
final Queue<String> contents = words("hi", "minecraft:pig");
final QueueAsStringReader reader = new QueueAsStringReader(contents);
assertEquals("hi", reader.readString());
reader.skipWhitespace();
assertEquals("minecraft", reader.readStringUntil(':'));
reader.updateQueue();
assertEquals(words("pig"), contents);
}
}

View file

@ -0,0 +1,159 @@
package cloud.commandframework.brigadier.argument;
import cloud.commandframework.types.tuples.Pair;
import com.mojang.brigadier.StringReader;
import org.junit.jupiter.api.Test;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for {@link StringReaderAsQueue}
*
* Most operations have 4 cases: 0 arguments, 1 argument, 2 arguments, and 3 or more arguments.
* At that point every whitespace handling path should be exercised.
*/
class StringReaderAsQueueTest {
@Test
void testIsNotEmpty() {
final StringReader reader = new StringReader("hello there");
final Queue<String> queue = StringReaderAsQueue.from(reader);
assertFalse(queue.isEmpty());
}
@Test
void testCreateEmpty() {
final Queue<String> queue = StringReaderAsQueue.from(new StringReader(""));
assertTrue(queue.isEmpty());
assertNull(queue.peek());
assertNull(queue.poll());
}
@Test
void testReadWord() {
final StringReader original = new StringReader("meow purr");
final Queue<String> queue = StringReaderAsQueue.from(original);
assertEquals("meow", queue.poll());
assertEquals("purr", original.getRemaining());
}
@Test
void testReadSingleWordContents() {
final StringReader reader = new StringReader("hello");
final Queue<String> queue = StringReaderAsQueue.from(reader);
assertFalse(queue.isEmpty());
assertEquals("hello", queue.peek());
assertEquals(1, queue.size());
}
@Test
void testReadTwoWords() {
final StringReader original = new StringReader("meow purr");
final Queue<String> queue = StringReaderAsQueue.from(original);
assertEquals("meow", queue.poll());
assertEquals("purr", queue.poll());
assertTrue(queue.isEmpty());
}
@Test
void testReadThreeWords() {
final Queue<String> queue = StringReaderAsQueue.from(new StringReader("we enjoy commands"));
assertEquals("we", queue.poll());
assertEquals("enjoy", queue.poll());
assertEquals("commands", queue.poll());
assertNull(queue.poll());
}
@Test
void testPeekRepeatedly() {
final Queue<String> queue = StringReaderAsQueue.from(new StringReader("commands are fun"));
for (int i = 0; i < 3; ++i) {
assertEquals("commands", queue.peek());
}
}
@Test
void testMultiElementIterator() {
final StringReader reader = new StringReader("tell @a :3");
final Iterator<String> elements = StringReaderAsQueue.from(reader).iterator();
assertTrue(elements.hasNext());
assertEquals("tell", elements.next());
assertTrue(elements.hasNext());
assertEquals("@a", elements.next());
assertTrue(elements.hasNext());
assertEquals(":3", elements.next());
assertFalse(elements.hasNext());
assertThrows(NoSuchElementException.class, elements::next);
}
@Test
void testDoubleElementIterator() {
final Iterator<String> elements = StringReaderAsQueue.from(new StringReader("cloud good")).iterator();
assertTrue(elements.hasNext());
assertEquals("cloud", elements.next());
assertTrue(elements.hasNext());
assertEquals("good", elements.next());
assertFalse(elements.hasNext());
assertThrows(NoSuchElementException.class, elements::next);
}
@Test
void testSingleElementIterator() {
final Iterator<String> elements = StringReaderAsQueue.from(new StringReader("word")).iterator();
assertTrue(elements.hasNext());
assertEquals("word", elements.next());
assertFalse(elements.hasNext());
assertThrows(NoSuchElementException.class, elements::next);
}
@Test
void testEmptyIterator() {
final Iterator<String> empty = StringReaderAsQueue.from(new StringReader("")).iterator();
assertFalse(empty.hasNext());
assertThrows(NoSuchElementException.class, empty::next);
}
@Test
void testPartlyStartedIteration() {
final Queue<String> queue = StringReaderAsQueue.from(new StringReader("let's go for a walk"));
assertEquals("let's", queue.poll());
final Iterator<String> it = queue.iterator();
assertEquals("go", it.next());
}
@Test
void testToArrayMultiple() {
final Queue<String> elements = StringReaderAsQueue.from(new StringReader("one two three four"));
assertArrayEquals(new String[] { "one", "two", "three", "four" }, elements.toArray());
}
@Test
void testSizes() {
Stream.of(
Pair.of("", 0),
Pair.of("hi", 1),
Pair.of("the second!", 2),
Pair.of("a third entry", 3),
Pair.of("one two three, four", 4)
).forEach(pair -> {
assertEquals(pair.getSecond(), StringReaderAsQueue.from(new StringReader(pair.getFirst())).size());
});
}
}

View file

@ -175,4 +175,8 @@ public class VelocityCommandManager<C> extends CommandManager<C> implements Brig
return this.commandSenderMapper;
}
final @NonNull Function<@NonNull C, @NonNull CommandSource> getBackwardsCommandSenderMapper() {
return this.backwardsCommandSenderMapper;
}
}

View file

@ -55,6 +55,7 @@ final class VelocityPluginRegistrationHandler<C> implements CommandRegistrationH
this.brigadierManager.brigadierSenderMapper(
sender -> this.manager.getCommandSenderMapper().apply(sender)
);
this.brigadierManager.backwardsBrigadierSenderMapper(this.manager.getBackwardsCommandSenderMapper());
}
@Override