From e77d5c8d783d2601e1d0656db1f25d5d10e8f281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Mon, 18 Jan 2021 16:36:25 +0100 Subject: [PATCH] Update README.adoc Add information about injection points, suggestions and custom arguments. --- docs/README.adoc | 217 ++++++++++++++++++++++++- docs/image-2021-01-18-16-23-02-480.png | Bin 0 -> 9765 bytes 2 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 docs/image-2021-01-18-16-23-02-480.png diff --git a/docs/README.adoc b/docs/README.adoc index 4edbc88a..fefbb281 100644 --- a/docs/README.adoc +++ b/docs/README.adoc @@ -296,6 +296,108 @@ commandBuilder.argumentTriplet( ==== Custom +Cloud allows you to create custom argument parsers. The easiest way to achieve this +is by extending `CommandArgument`. This is recommended if you are creating +arguments that will be exposed in some kind of library. For inspiration on how +to achieve this, it is recommended to check out the standard Cloud arguments. + +If you don't need to expose your parser as a part of an API, you may simply +implement `ArgumentParser`. The method you will be working with +looks like: + +[source,java] +---- +public @NonNull ArgumentParseResult parse( + @NonNull CommandContext<@NonNull C> commandContext, + @NonNull Queue<@NonNull String> inputQueue <1> +) { + // ... +} +---- +<1> Queue containing (remaining) user input. + +When reading an argument you should do the following: + +1. Peek the queue. +2. Attempt to parse your object. + * If the object could not be parsed, return `ArgumentParseResult.failure(exception)` +3. If the object was parsed successfully, pop a string from the queue. +4. Return `ArgumentParseResult.success(object)`. + +WARNING: If the read string(s) isn't popped from the queue, then the command engine will assume that the syntax is wrong and +send an error message to the command sender. + +It is highly recommended to make use of +https://javadoc.commandframework.cloud/cloud/commandframework/exceptions/parsing/ParserException.html[ParserException] +when returning a failed result. This allows for integration with the Cloud caption system (refer to the section on Exception +Handling for more information). + +You should - in most cases - register your parser to the +https://javadoc.commandframework.cloud/cloud/commandframework/arguments/parser/ParserRegistry.html[ParserRegistry] +which you can access using +https://javadoc.commandframework.cloud/cloud/commandframework/CommandManager.html#getParserRegistry()[CommandManager#getParserRegistry()]. +If you are registering a parser that shouldn't be the default for the +argument type, then it is recommended to register a named parser. +If your parser is not registered to the parser registry, it will +not be usable in annotated command methods. + +When registering a command parser, you're actually registering a +function that will generate a parser based on parser parameters. +These parameters can be used together with the annotation system +to differentiate between different parsers and also change parser +settings. In order to create these parameters you can create +an annotation mapper using +https://javadoc.commandframework.cloud/cloud/commandframework/arguments/parser/ParserRegistry.html#registerAnnotationMapper(java.lang.Class,java.util.function.BiFunction)[ParserRegistry#registerAnnotationMapper]. + +Here's an example of how a UUID parser can be created and registered: + +[title=Example UUID parser] +==== +This example is taken from +https://github.com/Incendo/cloud/blob/master/cloud-core/src/main/java/cloud/commandframework/arguments/standard/UUIDArgument.java[UUIDArgument.java] +, which also includes a custom exception and argument builder. +It's a good reference class for custom arguments, as it does +not contain any complicated logic. + +[source,java] +---- +public final class UUIDParser implements ArgumentParser { + + @Override + public @NonNull ArgumentParseResult parse( + final String input = inputQueue.peek(); + if (input == null) { + return ArgumentParseResult.failure(new NoInputProvidedException( + UUIDParser.class, + commandContext + )); + } + + try { + UUID uuid = UUID.fromString(input); + inputQueue.remove(); + return ArgumentParseResult.success(uuid); + } catch(final IllegalArgumentException e) { + return ArgumentParseResult.failure(new UUIDParseException(input, commandContext)); + } + ) + +} +---- + +It is then registered to the parser registry using +[source,java] +---- +parserRegistry.registerParserSupplier( + TypeToken.get(UUID.class), + options -> new UUIDParser<>() +); +---- +in +https://github.com/Incendo/cloud/blob/master/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java[StandardParserRegistry.java]. + +==== + ==== Flags Flags are named optional values that can either have an associated argument (value flag) or have the value evaluated by whether the flag is present (presence flag). These flags are registered much the same way as normal arguments, only that you use `.flag` methods in the command builder instead. @@ -358,15 +460,117 @@ manager.command( ==== Argument Preprocessing +An argument preprocessor is a function that gets to act on command +input before it's given to a command. This allows you to inject +custom verification behaviour into existing parsers, or register +annotations that add extra verification to your custom annotations. + +https://github.com/Incendo/cloud/blob/master/cloud-core/src/main/java/cloud/commandframework/arguments/preprocessor/RegexPreprocessor.java[RegexPreprocessor.java] +is a good example of a preprocessor that allows you to add regular +expression checking to your arguments. + +Argument preprocessors can be applied to created arguments using +https://javadoc.commandframework.cloud/cloud/commandframework/arguments/CommandArgument.html#addPreprocessor(java.util.function.BiFunction)[CommandArgument#addPreprocessor]. + === Suggestions +Many platforms support command suggestions. You can add command suggestions to your command parser, by overriding the suggestion +method: + +[source,java] +---- +@Override +public @NonNull List<@NonNull String> suggestions( + final @NonNull CommandContext commandContext, + final @NonNull String input +) { + final List completions = new ArrayList<>(); + for (Material value : Material.values()) { + completions.add(value.name().toLowerCase()); + } + return completions; +} +---- + +or by specifying a suggestion function in a command argument builder +using +https://javadoc.commandframework.cloud/cloud/commandframework/arguments/CommandArgument.Builder.html#withSuggestionsProvider(java.util.function.BiFunction)[CommandArgument.Builder#withSuggestionProvider]. + +You also register a standalone suggestions to the parser registry, +using +https://javadoc.commandframework.cloud/cloud/commandframework/arguments/parser/ParserRegistry.html#registerSuggestionProvider(java.lang.String,java.util.function.BiFunction)[ParserRegistry#registerSuggestionProvider]. +Registering a named suggestion provider allows it to be used in +annotated command methods, or retrieved using `ParserRegistry#getSuggestionProvider`. + === Injection Points +image::image-2021-01-18-16-23-02-480.png[Execution Pipeline] + +When a command is entered by a command sender, it goes through +the following stages: + +1. It is turned into string tokens. This is used to create the input queue. +2. A command context is created for the input queue combined with the command sender. +3. The command is passed to the preprocessors, which may alter the input queue or write to the context. + * If a preprocessor causes an interrupt using `ConsumerService.interrupt()` then the context will be filtered out and the +command will not be parsed. + * If no preprocessor filters out the context, the context and input will be ready to be parsed into an executable command. +4. The input is parsed into a command chain and components are written +to the context. + * If the command does not fit any existing command chains, the sender is notified and the parsing is cancelled. + * If the command is valid, it will be sent to the postprocessors. +5. The command postprocessors get to act on the command can alter the command context. they may now postpone command execution, +such is the case with the command confirmation postprocessor. + * If a postprocessor causes an interrupt using `ConsumerService.interupt()` the command will not be executed. + * If no postprocessor interrupts during the post-processing stage, the command will be sent to the executor. +6. The command is executed using the command executor. + ==== Preprocessing +Command preprocessing happens before the input has been pasted to the command tree for parsing. To register a preprocessor, implement `cloud.commandframework.execution.preprocessor.CommandPreProcessor`: + +[source,java] +---- +public class YourPreProcessor implements CommandPreprocessor { + + @Override + public void accept(final CommandPreprocessingContext context) { + /* Act on the context */ + if (yourCondition) { + /* Filter out the context so that it is never passed to the parser */ + ConsumerService.interrupt(); + } + } + +} +---- + +Then register the preprocessor using `CommandManager#registerCommandPreProcessor(CommandPreprocessor)`. + ==== Postprocessing +Command postprocessing happen after the input has been parsed into a command chain, but before the command is executed. To register a postprocessor, implement `cloud.commandframework.execution.postprocessor.CommandPostProcessor`: + +[source,java] +---- +public class YourPostprocessor implements CommandPostprocessor { + + @Override + public void accept(final CommandPostprocessingContext context) { + /* Act on the context */ + if (yourCondition) { + /* Filter out the context so that it is never passed to the executor */ + ConsumerService.interrupt(); + } + } + +} +---- + +Then register the postprocessor using `CommandManager#registerCommandPostProcessor(CommandPostprocessor)`. + === Execution Coordinators +TODO === Command Proxies @@ -390,6 +594,7 @@ in the order they are declared in the proxied command. Furthermore, the proxied command that is currently being built. If the current command builder does not have a permission node set, this too will be copied. === Permissions +TODO === Exception Handling @@ -444,10 +649,6 @@ if (registry instanceof FactoryDelegatingCaptionRegistry) { ---- ==== -=== Command Context - -=== Command Handler - === Extra ==== Confirmations @@ -487,6 +688,7 @@ that require confirmation needs `.meta(CommandConfirmationManager.META_CONFIRMAT or a `@Confirmation` annotation. ==== Help Generation +TODO == Annotations @@ -749,6 +951,7 @@ annotationParser.registerPreprocessorMapper( ---- == Kotlin DSL +TODO == Platforms @@ -842,7 +1045,7 @@ and then initialize the asynchronous completion listener by using `paperCommandM ==== Sponge -The Sponge implementation is still a work in progress. +TODO ==== Fabric @@ -940,12 +1143,16 @@ You can also create your own mappings. See the platform adapter JavaDoc for more information. === Discord +TODO ==== JDA +TODO ==== Javacord +TODO === IRC +TODO [glossary] == Glossary diff --git a/docs/image-2021-01-18-16-23-02-480.png b/docs/image-2021-01-18-16-23-02-480.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd8e2157bb5525fa5eb6e3868e7bd853bd3cf95 GIT binary patch literal 9765 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 p;j