Skip to content

Commit ac84d75

Browse files
committed
Rework Parser definition
Instead of setting up logic via builders, parsers are usually just lambdas. The result of parsers can be adapted using `Result#continueWith`. Also fixed inconsistency with vanilla behavior in `IntegerRange` parsing brought up in CommandAPI#575 (comment).
1 parent ba8d578 commit ac84d75

File tree

13 files changed

+530
-954
lines changed

13 files changed

+530
-954
lines changed

commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/ArgumentUtilities.java

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44
import com.mojang.brigadier.StringReader;
55
import com.mojang.brigadier.exceptions.BuiltInExceptionProvider;
66
import com.mojang.brigadier.exceptions.CommandSyntaxException;
7-
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
87
import dev.jorel.commandapi.BukkitTooltip;
9-
import dev.jorel.commandapi.arguments.parser.Parser;
8+
import dev.jorel.commandapi.arguments.parser.ParserArgument;
9+
import dev.jorel.commandapi.arguments.parser.ParserLiteral;
1010
import net.kyori.adventure.text.Component;
1111
import net.kyori.adventure.text.TextComponent;
1212

13-
import java.util.function.Function;
14-
1513
/**
1614
* Utilities for creating mock argument parsers
1715
*/
@@ -53,42 +51,17 @@ public static Message translatedMessage(String key, Object... args) {
5351
}
5452

5553
// Parser utilities
56-
/**
57-
* A placeholder {@link CommandSyntaxException} that a parser can throw if it doesn't match the input.
58-
* Typically, this exception will be caught immediately using {@link Parser.ExceptionHandler#neverThrowException()},
59-
* and parsing will continue to the next branch in a {@link Parser#tryParse(Parser.NonTerminal)} chain.
60-
*/
61-
public static final CommandSyntaxException NEXT_BRANCH = new SimpleCommandExceptionType(
62-
() -> "This branch did not match"
63-
).create();
64-
65-
/**
66-
* Returns a new {@link Parser.Literal}. When the returned parser is invoked, if {@link StringReader#canRead()}
67-
* returns {@code false}, then a {@link CommandSyntaxException} will be thrown according to the given {@code exception}
68-
* {@link Function}. If {@link StringReader#canRead()} returns {@code true}, then the returned parser succeeds.
69-
*
70-
* @param exception A {@link Function} that creates a {@link CommandSyntaxException} when the input
71-
* {@link StringReader} does not have any more characters to read.
72-
* @return A {@link Parser.Literal} that checks if the input {@link StringReader} has characters to read.
73-
*/
74-
public static Parser.Literal assertCanRead(Function<StringReader, CommandSyntaxException> exception) {
75-
return reader -> {
76-
if (!reader.canRead()) {
77-
throw exception.apply(reader);
78-
}
79-
};
80-
}
8154

8255
/**
83-
* Returns a new {@link Parser.Literal}. When the returned parser is invoked, it tries to read the given
56+
* Returns a new {@link ParserLiteral}. When the returned parser is invoked, it tries to read the given
8457
* {@code literal} String from the input {@link StringReader}. If the {@code literal} is present, this parser
8558
* succeeds and moves {@link StringReader#getCursor()} to the end of the {@code literal}. Otherwise, this parser
8659
* will fail and throw a {@link CommandSyntaxException} with type {@link BuiltInExceptionProvider#literalIncorrect()}.
8760
*
8861
* @param literal The exact String that is expected to be at the start of the input {@link StringReader}.
89-
* @return A {@link Parser.Literal} that checks if the {@code literal} String can be read from the input {@link StringReader}.
62+
* @return A {@link ParserLiteral} that checks if the {@code literal} String can be read from the input {@link StringReader}.
9063
*/
91-
public static Parser.Literal literal(String literal) {
64+
public static ParserLiteral literal(String literal) {
9265
return reader -> {
9366
if (reader.canRead(literal.length())) {
9467
int start = reader.getCursor();
@@ -104,7 +77,7 @@ public static Parser.Literal literal(String literal) {
10477
}
10578

10679
/**
107-
* Returns a new {@link Parser.Argument} that reads characters from the input {@link StringReader} until it reaches
80+
* Returns a new {@link ParserArgument} that reads characters from the input {@link StringReader} until it reaches
10881
* the given terminator character. If the terminator character is not found, the entire
10982
* {@link StringReader#getRemaining()} String will be read.
11083
* <p>
@@ -116,11 +89,11 @@ public static Parser.Literal literal(String literal) {
11689
* if this happens.
11790
*
11891
* @param terminator The character to stop reading at.
119-
* @return A {@link Parser.Argument} that reads until it finds the given terminator. Note that the returned String will
92+
* @return A {@link ParserArgument} that reads until it finds the given terminator. Note that the returned String will
12093
* include the terminator at the end, unless the end of the input {@link StringReader} is reached without finding the
12194
* terminator.
12295
*/
123-
public static Parser.Argument<String> readUntilWithoutEscapeCharacter(char terminator) {
96+
public static ParserArgument<String> readUntilWithoutEscapeCharacter(char terminator) {
12497
return reader -> {
12598
int start = reader.getCursor();
12699

commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorArgumentType.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static EntitySelectorArgumentType player() {
5151

5252
@Override
5353
public EntitySelector parse(StringReader reader) throws CommandSyntaxException {
54-
EntitySelector entityselector = EntitySelectorParser.PARSER.parse(reader);
54+
EntitySelector entityselector = EntitySelectorParser.parser.parse(reader);
5555
// I don't know why Minecraft does `reader.setCursor(0)` here before throwing exceptions, but it does ¯\_(ツ)_/¯
5656
// That has the goofy result of underlining the whole command when it should really only underline the selector
5757
// This is easily fixed, just store `reader.getCursor()` before parsing the selector
@@ -73,7 +73,7 @@ public EntitySelector parse(StringReader reader) throws CommandSyntaxException {
7373

7474
@Override
7575
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
76-
return EntitySelectorParser.PARSER.listSuggestions(context, builder);
76+
return EntitySelectorParser.parser.listSuggestions(context, builder);
7777
}
7878

7979
public static List<? extends Entity> findManyEntities(CommandContext<MockCommandSource> cmdCtx, String key, boolean allowEmpty) throws CommandSyntaxException {

commandapi-platforms/commandapi-bukkit/commandapi-bukkit-test-toolkit/src/main/java/dev/jorel/commandapi/arguments/EntitySelectorParser.java

Lines changed: 49 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
44
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
55
import dev.jorel.commandapi.UnimplementedMethodException;
6-
import dev.jorel.commandapi.arguments.parser.ParameterGetter;
76
import dev.jorel.commandapi.arguments.parser.Parser;
7+
import dev.jorel.commandapi.arguments.parser.ParserLiteral;
8+
import dev.jorel.commandapi.arguments.parser.Result;
89
import dev.jorel.commandapi.arguments.parser.SuggestionProvider;
910
import org.bukkit.Bukkit;
1011
import org.bukkit.entity.Entity;
1112
import org.bukkit.entity.EntityType;
1213

1314
import java.util.UUID;
15+
import java.util.function.Function;
1416
import java.util.function.Predicate;
1517

1618
public class EntitySelectorParser {
@@ -48,16 +50,12 @@ private EntitySelector build() {
4850
}
4951

5052
// Parsing
51-
private static final Parser.Literal isSelectorStart = reader -> {
52-
if (!(reader.canRead() && reader.peek() == '@')) throw ArgumentUtilities.NEXT_BRANCH;
53-
};
54-
55-
private static Parser.Literal parseSelector(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
56-
return reader -> {
53+
private static ParserLiteral parseSelector(EntitySelectorParser selectorBuilder) {
54+
return Parser.read(reader -> {
5755
reader.skip(); // skip @
5856
if (!reader.canRead()) throw ERROR_MISSING_SELECTOR_TYPE.createWithContext(reader);
5957
char selectorCode = reader.read();
60-
EntitySelectorParser selectorBuilder = selectorBuilderGetter.get();
58+
6159
switch (selectorCode) {
6260
case 'p' -> {
6361
selectorBuilder.maxResults = 1;
@@ -101,29 +99,19 @@ private static Parser.Literal parseSelector(ParameterGetter<EntitySelectorParser
10199
throw ERROR_UNKNOWN_SELECTOR_TYPE.createWithContext(reader, "@" + selectorCode);
102100
}
103101
}
104-
};
102+
}).suggests(suggestSelector);
105103
}
106104

107-
private static final Parser.Literal isSelectorOptionsStart = reader -> {
108-
if (!(reader.canRead() && reader.peek() == '[')) throw ArgumentUtilities.NEXT_BRANCH;
109-
};
110-
111-
private static Parser.Literal parseSelectorOptions(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
105+
private static ParserLiteral parseSelectorOptions(EntitySelectorParser selectorBuilder) {
112106
return reader -> {
113107
// TODO: Implement looping to parse these selector options
114108
// I'm pretty sure it would basically reuse many other object parsers as well, so maybe do those first
115109
throw new UnimplementedMethodException("Entity selectors with options are not supported");
116110
};
117111
}
118112

119-
private static final Parser.Literal isNameStart = reader -> {
120-
if (!(reader.canRead() && reader.peek() != ' ')) throw ERROR_INVALID_NAME_OR_UUID.createWithContext(reader);
121-
};
122-
123-
private static Parser.Literal parseNameOrUUID(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
124-
return reader -> {
125-
EntitySelectorParser selectorBuilder = selectorBuilderGetter.get();
126-
113+
private static ParserLiteral parseNameOrUUID(EntitySelectorParser selectorBuilder) {
114+
return Parser.read(reader -> {
127115
int start = reader.getCursor();
128116
String input = reader.readString();
129117
try {
@@ -132,7 +120,7 @@ private static Parser.Literal parseNameOrUUID(ParameterGetter<EntitySelectorPars
132120
selectorBuilder.includesEntities = true;
133121
} catch (IllegalArgumentException ignored) {
134122
// Not a valid UUID string
135-
if (input.length() > 16) {
123+
if (input.isEmpty() || input.length() > 16) {
136124
// Also not a valid player name
137125
reader.setCursor(start);
138126
throw ERROR_INVALID_NAME_OR_UUID.createWithContext(reader);
@@ -143,11 +131,7 @@ private static Parser.Literal parseNameOrUUID(ParameterGetter<EntitySelectorPars
143131
}
144132

145133
selectorBuilder.maxResults = 1;
146-
};
147-
}
148-
149-
private static Parser.Argument<EntitySelector> conclude(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
150-
return reader -> selectorBuilderGetter.get().build();
134+
}).suggests(suggestName);
151135
}
152136

153137
private static final SuggestionProvider suggestName = (context, builder) -> {
@@ -173,44 +157,43 @@ private static Parser.Argument<EntitySelector> conclude(ParameterGetter<EntitySe
173157
suggestName.addSuggestions(context, builder);
174158
};
175159
private static final SuggestionProvider suggestOpenOptions = (context, builder) -> builder.suggest("[");
176-
private static final SuggestionProvider suggestOptionsKeyOrClose = (context, builder) -> {
177-
throw new UnimplementedMethodException("Entity selectors with options are not supported");
178-
};
179160

180-
public static final Parser<EntitySelector> PARSER = Parser
181-
.parse(reader -> new EntitySelectorParser())
182-
.suggests(suggestNameOrSelector)
183-
.alwaysThrowException()
184-
.continueWith(selectorBuilder ->
185-
Parser.tryParse(Parser.read(isSelectorStart)
186-
.neverThrowException()
187-
.continueWith(
188-
Parser.read(parseSelector(selectorBuilder))
189-
.suggests(suggestSelector)
190-
.alwaysThrowException()
191-
.continueWith(
192-
Parser.tryParse(Parser.read(isSelectorOptionsStart)
193-
.suggests(suggestOpenOptions)
194-
.neverThrowException()
195-
.continueWith(
196-
Parser.read(parseSelectorOptions(selectorBuilder))
197-
.suggests(suggestOptionsKeyOrClose)
198-
.alwaysThrowException()
199-
// Input @?[???]
200-
.continueWith(conclude(selectorBuilder))
201-
)
202-
).then(conclude(selectorBuilder)) // Input @?
203-
)
204-
)
205-
).then(Parser.read(isNameStart)
206-
.alwaysThrowException()
207-
.continueWith(
208-
Parser.read(parseNameOrUUID(selectorBuilder))
209-
.suggests(suggestName)
210-
.alwaysThrowException()
211-
// Input name or uuid
212-
.continueWith(conclude(selectorBuilder))
213-
)
214-
)
161+
public static final Parser<EntitySelector> parser = reader -> {
162+
if (!reader.canRead()) {
163+
// Empty input
164+
return Result.withExceptionAndSuggestions(ERROR_INVALID_NAME_OR_UUID.createWithContext(reader), reader.getCursor(), suggestNameOrSelector);
165+
}
166+
167+
// Build our selector
168+
EntitySelectorParser selectorBuilder = new EntitySelectorParser();
169+
Function<Result.Void, Result<EntitySelector>> conclude = Result.wrapFunctionResult(success -> selectorBuilder.build());
170+
171+
if (reader.peek() == '@') {
172+
// Looks like selector
173+
return parseSelector(selectorBuilder).getResult(reader).continueWith(
174+
// Successfully read selector
175+
success -> {
176+
if (reader.canRead() && reader.peek() == '[') {
177+
// Looks like includes selector options
178+
return parseSelectorOptions(selectorBuilder).getResult(reader).continueWith(
179+
// If successful, build the final selector
180+
conclude
181+
// Otherwise, pass original exception
182+
);
183+
}
184+
185+
// Otherwise, valid selector, but suggest opening options
186+
return Result.withValueAndSuggestions(selectorBuilder.build(), reader.getCursor(), suggestOpenOptions);
187+
}
188+
// Otherwise pass original exception
189+
);
190+
}
191+
192+
// Looks like name/uuid
193+
return parseNameOrUUID(selectorBuilder).getResult(reader).continueWith(
194+
// If successful, build the final selector
195+
conclude
196+
// Otherwise pass original exception
215197
);
198+
};
216199
}

0 commit comments

Comments
 (0)