Skip to content

Commit 07bc9c9

Browse files
authored
Merge pull request CommandAPI#540 from JorelAli/dev/safe-arguments
Implement "safe-casting"
2 parents 698e696 + a48f1a6 commit 07bc9c9

File tree

6 files changed

+286
-3
lines changed

6 files changed

+286
-3
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ This is the current roadmap for the CommandAPI (as of 11th May 2023):
428428
<li>Fixed implementation issues with <code>FunctionArgument</code></li>
429429
<li>https://github.com/JorelAli/CommandAPI/issues/490 Adds (experimental) support for Mojang-mapped servers via the CommandAPI config</li>
430430
<li>https://github.com/JorelAli/CommandAPI/issues/524 Fixes <code>CommandAPIBukkit.get().getTags()</code> erroring in 1.20.4</li>
431+
<li>https://github.com/JorelAli/CommandAPI/issues/536, https://github.com/JorelAli/CommandAPI/pull/537 Fixes <code>MultiLiteralArgument</code> help displaying the node name instead of the literal text</li>
432+
<li>https://github.com/JorelAli/CommandAPI/pull/540 Add methods to "safe-cast" arguments to <code>CommandArguments</code></li>
431433
</ul>
432434
<b>Minecraft Version Changes:</b>
433435
<ul>

commandapi-core/src/main/java/dev/jorel/commandapi/executors/CommandArguments.java

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package dev.jorel.commandapi.executors;
22

3+
import dev.jorel.commandapi.arguments.AbstractArgument;
4+
35
import javax.annotation.Nullable;
46

57
import java.util.Collection;
68
import java.util.Collections;
9+
import java.util.HashMap;
710
import java.util.Map;
811
import java.util.Optional;
912
import java.util.function.Supplier;
@@ -46,6 +49,17 @@ public record CommandArguments(
4649
String fullInput
4750
) {
4851

52+
private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER = Map.of(
53+
boolean.class, Boolean.class,
54+
char.class, Character.class,
55+
byte.class, Byte.class,
56+
short.class, Short.class,
57+
int.class, Integer.class,
58+
long.class, Long.class,
59+
float.class, Float.class,
60+
double.class, Double.class
61+
);
62+
4963
// Access the inner structure directly
5064

5165
/**
@@ -79,7 +93,7 @@ public String getFullInput() {
7993
public int count() {
8094
return args.length;
8195
}
82-
96+
8397
// Main accessing methods. In Kotlin, methods named get() allows it to
8498
// access these methods using array notation, as a part of operator overloading.
8599
// More information about operator overloading in Kotlin can be found here:
@@ -342,7 +356,7 @@ public Optional<String> getRawOptional(String nodeName) {
342356
}
343357
return Optional.of(rawArgsMap.get(nodeName));
344358
}
345-
359+
346360
/** Unchecked methods. These are the same as the methods above, but use
347361
* unchecked generics to conform to the type they are declared as. In Java,
348362
* the normal methods (checked) require casting:
@@ -367,7 +381,7 @@ public Optional<String> getRawOptional(String nodeName) {
367381
public <T> T getUnchecked(int index) {
368382
return (T) get(index);
369383
}
370-
384+
371385
/**
372386
* Returns an argument by its node name
373387
*
@@ -443,4 +457,147 @@ public <T> Optional<T> getOptionalUnchecked(String nodeName) {
443457
return (Optional<T>) getOptional(nodeName);
444458
}
445459

460+
/*****************************************
461+
********** SAFE-CAST ARGUMENTS **********
462+
*****************************************/
463+
464+
/**
465+
* Returns an argument purely based on its CommandAPI representation. This also attempts to directly cast the argument to the type represented by {@link dev.jorel.commandapi.arguments.AbstractArgument#getPrimitiveType()}
466+
*
467+
* @param argumentType The argument instance used to create the argument
468+
* @return The argument represented by the CommandAPI argument, or null if the argument was not found.
469+
*/
470+
@Nullable
471+
public <T> T getByArgument(AbstractArgument<T, ?, ?, ?> argumentType) {
472+
return castArgument(get(argumentType.getNodeName()), argumentType.getPrimitiveType(), argumentType.getNodeName());
473+
}
474+
475+
/**
476+
* Returns an argument purely based on its CommandAPI representation or a default value if the argument wasn't found.
477+
* <p>
478+
* If the argument was found, this also attempts to directly cast the argument to the type represented by {@link dev.jorel.commandapi.arguments.AbstractArgument#getPrimitiveType()}
479+
*
480+
* @param argumentType The argument instance used to create the argument
481+
* @param defaultValue The default value to return if the argument wasn't found
482+
* @return The argument represented by the CommandAPI argument, or the default value if the argument was not found.
483+
*/
484+
public <T> T getByArgumentOrDefault(AbstractArgument<T, ?, ?, ?> argumentType, T defaultValue) {
485+
T argument = getByArgument(argumentType);
486+
return (argument != null) ? argument : defaultValue;
487+
}
488+
489+
/**
490+
* Returns an <code>Optional</code> holding the provided argument. This <code>Optional</code> can be empty if the argument was not given when running the command.
491+
* <p>
492+
* This attempts to directly cast the argument to the type represented by {@link dev.jorel.commandapi.arguments.AbstractArgument#getPrimitiveType()}
493+
*
494+
* @param argumentType The argument instance used to create the argument
495+
* @return An <code>Optional</code> holding the argument, or an empty <code>Optional</code> if the argument was not found.
496+
*/
497+
public <T> Optional<T> getOptionalByArgument(AbstractArgument<T, ?, ?, ?> argumentType) {
498+
return Optional.ofNullable(getByArgument(argumentType));
499+
}
500+
501+
/**
502+
* Returns an argument based on its node name. This also attempts to directly cast the argument to the type represented by the {@code argumentType} parameter.
503+
*
504+
* @param nodeName The node name of the argument
505+
* @param argumentType The class that represents the argument
506+
* @return The argument with the given node name, or null if the argument was not found.
507+
*/
508+
@Nullable
509+
public <T> T getByClass(String nodeName, Class<T> argumentType) {
510+
return castArgument(get(nodeName), argumentType, nodeName);
511+
}
512+
513+
/**
514+
* Returns an argument based on its node name or a default value if the argument wasn't found.
515+
* <p>
516+
* If the argument was found, this method attempts to directly cast the argument to the type represented by the {@code argumentType} parameter.
517+
*
518+
* @param nodeName The node name of the argument
519+
* @param argumentType The class that represents the argument
520+
* @param defaultValue The default value to return if the argument wasn't found
521+
* @return The argument with the given node name, or the default value if the argument was not found.
522+
*/
523+
public <T> T getByClassOrDefault(String nodeName, Class<T> argumentType, T defaultValue) {
524+
T argument = getByClass(nodeName, argumentType);
525+
return (argument != null) ? argument : defaultValue;
526+
}
527+
528+
/**
529+
* Returns an <code>Optional</code> holding the argument with the given node name. This <code>Optional</code> can be empty if the argument was not given when running the command.
530+
* <p>
531+
* This attempts to directly cast the argument to the type represented by the {@code argumentType} parameter.
532+
*
533+
* @param nodeName The node name of the argument
534+
* @param argumentType The class that represents the argument
535+
* @return An <code>Optional</code> holding the argument, or an empty <code>Optional</code> if the argument was not found.
536+
*/
537+
public <T> Optional<T> getOptionalByClass(String nodeName, Class<T> argumentType) {
538+
return Optional.ofNullable(getByClass(nodeName, argumentType));
539+
}
540+
541+
/**
542+
* Returns an argument based on its index. This also attempts to directly cast the argument to the type represented by the {@code argumentType} parameter.
543+
*
544+
* @param index The index of the argument
545+
* @param argumentType The class that represents the argument
546+
* @return The argument at the given index, or null if the argument was not found.
547+
*/
548+
@Nullable
549+
public <T> T getByClass(int index, Class<T> argumentType) {
550+
return castArgument(get(index), argumentType, index);
551+
}
552+
553+
/**
554+
* Returns an argument based on its index or a default value if the argument wasn't found.
555+
* <p>
556+
* If the argument was found, this method attempts to directly cast the argument to the type represented by the {@code argumentType} parameter.
557+
*
558+
* @param index The index of the argument
559+
* @param argumentType The class that represents the argument
560+
* @param defaultValue The default value to return if the argument wasn't found
561+
* @return The argument at the given index, or the default value if the argument was not found.
562+
*/
563+
public <T> T getByClassOrDefault(int index, Class<T> argumentType, T defaultValue) {
564+
T argument = getByClass(index, argumentType);
565+
return (argument != null) ? argument : defaultValue;
566+
}
567+
568+
/**
569+
* Returns an <code>Optional</code> holding the argument at the given index. This <code>Optional</code> can be empty if the argument was not given when running the command.
570+
* <p>
571+
* This attempts to directly cast the argument to the type represented by the {@code argumentType} parameter.
572+
*
573+
* @param index The index of the argument
574+
* @param argumentType The class that represents the argument
575+
* @return An <code>Optional</code> holding the argument, or an empty <code>Optional</code> if the argument was not found.
576+
*/
577+
public <T> Optional<T> getOptionalByClass(int index, Class<T> argumentType) {
578+
return Optional.ofNullable(getByClass(index, argumentType));
579+
}
580+
581+
private <T> T castArgument(Object argument, Class<T> argumentType, Object argumentNameOrIndex) {
582+
if (argument == null) {
583+
return null;
584+
}
585+
if (!PRIMITIVE_TO_WRAPPER.getOrDefault(argumentType, argumentType).isAssignableFrom(argument.getClass())) {
586+
throw new IllegalArgumentException(buildExceptionMessage(argumentNameOrIndex, argument.getClass().getSimpleName(), argumentType.getSimpleName()));
587+
}
588+
return (T) argument;
589+
}
590+
591+
private String buildExceptionMessage(Object argumentNameOrIndex, String expectedClass, String actualClass) {
592+
if (argumentNameOrIndex instanceof Integer i) {
593+
return "Argument at index '" + i + "' is defined as " + expectedClass + ", not " + actualClass;
594+
}
595+
if (argumentNameOrIndex instanceof String s) {
596+
return "Argument '" + s + "' is defined as " + expectedClass + ", not " + actualClass;
597+
}
598+
throw new IllegalStateException("Unexpected behaviour detected while building exception message!" +
599+
"This should never happen - if you're seeing this message, please" +
600+
"contact the developers of the CommandAPI, we'd love to know how you managed to get this error!");
601+
}
602+
446603
}

commandapi-documentation-code/src/main/java/dev/jorel/commandapi/examples/java/Examples.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,31 @@ void commandArguments() {
13661366
})
13671367
.register();
13681368
/* ANCHOR_END: commandArguments3 */
1369+
1370+
/* ANCHOR: commandArguments4 */
1371+
StringArgument nameArgument = new StringArgument("name");
1372+
IntegerArgument amountArgument = new IntegerArgument("amount");
1373+
PlayerArgument playerArgument = new PlayerArgument("player");
1374+
PlayerArgument targetArgument = new PlayerArgument("target");
1375+
GreedyStringArgument messageArgument = new GreedyStringArgument("message");
1376+
1377+
new CommandAPICommand("mycommand")
1378+
.withArguments(nameArgument)
1379+
.withArguments(amountArgument)
1380+
.withOptionalArguments(playerArgument)
1381+
.withOptionalArguments(targetArgument)
1382+
.withOptionalArguments(messageArgument)
1383+
.executesPlayer((player, args) -> {
1384+
String name = args.getByArgument(nameArgument);
1385+
int amount = args.getByArgument(amountArgument);
1386+
Player p = args.getByArgumentOrDefault(playerArgument, player);
1387+
Player target = args.getByArgumentOrDefault(targetArgument, player);
1388+
String message = args.getOptionalByArgument(messageArgument).orElse("Hello!");
1389+
1390+
// Do whatever with these values
1391+
})
1392+
.register();
1393+
/* ANCHOR_END: commandArguments4 */
13691394
}
13701395

13711396
void commandFailures() {

commandapi-documentation-code/src/main/kotlin/dev/jorel/commandapi/examples/kotlin/Examples.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,31 @@ CommandAPICommand("mycommand")
12871287
})
12881288
.register();
12891289
/* ANCHOR_END: commandArguments3 */
1290+
1291+
/* ANCHOR: commandArguments4 */
1292+
val nameArgument = StringArgument("name")
1293+
val amountArgument = IntegerArgument("amount")
1294+
val playerArgument = PlayerArgument("player")
1295+
val targetArgument = PlayerArgument("target")
1296+
val messageArgument = GreedyStringArgument("message")
1297+
1298+
CommandAPICommand("mycommand")
1299+
.withArguments(nameArgument)
1300+
.withArguments(amountArgument)
1301+
.withOptionalArguments(playerArgument)
1302+
.withOptionalArguments(targetArgument)
1303+
.withOptionalArguments(messageArgument)
1304+
.executesPlayer(PlayerCommandExecutor { player, args ->
1305+
val name: String = args.getByArgument(nameArgument)!!
1306+
val amount: Int = args.getByArgument(amountArgument)!!
1307+
val p: Player = args.getByArgumentOrDefault(playerArgument, player)
1308+
val target: Player = args.getByArgumentOrDefault(targetArgument, player)
1309+
val message: String = args.getOptionalByArgument(messageArgument).orElse("Hello!")
1310+
1311+
// Do whatever with these values
1312+
})
1313+
.register();
1314+
/* ANCHOR_END: commandArguments4 */
12901315
}
12911316

12921317
fun commandFailures() {

docssrc/src/commandarguments.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ While the argument array just gives the possibility to access the arguments via
88
- [Access arguments](#access-arguments)
99
- [Access raw arguments](#access-raw-arguments)
1010
- [Access unsafe arguments](#access-unsafe-arguments)
11+
- [Access safe arguments](#access-safe-arguments)
1112

1213
-----
1314

@@ -254,3 +255,75 @@ Here, we don't actually want to cast the argument, so we use unsafe arguments to
254255
</div>
255256

256257
</div>
258+
259+
-----
260+
261+
## Access safe arguments
262+
263+
<div class="warning">
264+
265+
**Developer's Note:**
266+
267+
The following methods cannot be used to access a value returned by a `CustomArgument` as its return type depends on the base argument for it.
268+
269+
</div>
270+
271+
Lastly, the CommandArguments class offers you a way to access your arguments in a more safe way by using internal casts. Again, methods are offered to access arguments by their
272+
index or their node name:
273+
274+
```java
275+
T getByClass(String nodeName, Class<T> argumentType);
276+
T getByClassOrDefault(String nodeName, Class<T> argumentType, T defaultValue);
277+
T getOptionalByClass(String nodeName, Class<T> argumentType);
278+
T getByClass(int index, Class<T> argumentType);
279+
T getByClassOrDefault(int index, Class<T> argumentType, T defaultValue);
280+
T getOptionalByClass(int index, Class<T> argumentType);
281+
```
282+
283+
Compared to the other methods the `CommandArguments` class offers, these methods take an additional parameter of type `Class<T>` where `T` is the return type
284+
of the argument with the given node name or index.
285+
286+
For example, say you declared a `new StringArgument("value")` and you now want to access the return value of this argument using safe casting. This would be done as follows:
287+
288+
<div class="multi-pre">
289+
290+
```java,Java
291+
String value = args.getByClass("value", String.class);
292+
```
293+
294+
```kotlin,Kotlin
295+
val value = args.getByClass("value", String::class.java)
296+
```
297+
298+
</div>
299+
300+
### Access safe arguments using an argument instance
301+
302+
Finally, there is one more, even safer way of accessing safe arguments: by using an argument instance:
303+
304+
```java
305+
T getByArgument(Argument<T> argumentType);
306+
T getByArgumentOrDefault(Argument<T> argumentType, T defaultValue);
307+
T getOptionalByArgument(Argument<T> argumentType);
308+
```
309+
310+
However, while safer, this also introduces the need to first initialize your arguments before you can start implementing your command.
311+
To visualize this, we want to implement the command from [Access arguments by node name and index](#example---access-arguments-by-node-name-and-index) again, but this time using safe arguments with an argument instance:
312+
313+
<div class="example">
314+
315+
### Example - Access safe arguments using an argument instance
316+
317+
<div class="multi-pre">
318+
319+
```java,Java
320+
{{#include ../../commandapi-documentation-code/src/main/java/dev/jorel/commandapi/examples/java/Examples.java:commandArguments4}}
321+
```
322+
323+
```kotlin,Kotlin
324+
{{#include ../../commandapi-documentation-code/src/main/kotlin/dev/jorel/commandapi/examples/kotlin/Examples.kt:commandArguments4}}
325+
```
326+
327+
</div>
328+
329+
</div>

docssrc/src/intro.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Here's the list of changes to the documentation between each update. You can vie
4040
### Documentation changes 9.3.0 \\(\rightarrow\\) 9.4.0
4141

4242
- Adds [Velocity](./velocity_intro.md) page to outline how to setup the CommandAPI for Velocity
43+
- Updates [CommandArguments](./commandarguments.md) to document new additions for safe arguments
4344

4445
### Documentation changes 9.2.0 \\(\rightarrow\\) 9.3.0
4546

0 commit comments

Comments
 (0)