Skip to content

Commit

Permalink
feat(core): flag yielding arguments (#367)
Browse files Browse the repository at this point in the history
Implements #218
Also confirmed to fix #321.

Both `StringArgument` and `StringArrayArgument` now have flag-yielding modes. For annotated command method users, this can be activated using ´@FlagYielding`
  • Loading branch information
Citymonstret authored and jpenilla committed Jun 19, 2022
1 parent e889811 commit d4ab593
Show file tree
Hide file tree
Showing 11 changed files with 558 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Core: Add `DurationArgument` for parsing `java.time.Duration` ([#330](https://github.com/Incendo/cloud/pull/330))
- Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363))
- Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363))
- Core: Add flag yielding modes to `StringArgument` and `StringArrayArgument` ([#367](https://github.com/Incendo/cloud/pull/367))
- Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353))
- Annotations: `@CommandContainer` annotation processing ([#364](https://github.com/Incendo/cloud/pull/364))
- Annotations: `@CommandMethod` annotation processing for compile-time validation ([#365](https://github.com/Incendo/cloud/pull/365))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// MIT License
//
// Copyright (c) 2021 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.annotations.specifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates that the argument should stop parsing when encountering what
* could potentially be a flag.
* <p>
* This only has an effect on greedy arguments that consume all remaining input.
*
* @since 1.7.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface FlagYielding {

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@
*/
public final class FlagArgument<C> extends CommandArgument<C, Object> {

private static final Pattern FLAG_ALIAS_PATTERN = Pattern.compile(" -(?<name>([A-Za-z]+))");
private static final Pattern FLAG_PRIMARY_PATTERN = Pattern.compile(" --(?<name>([A-Za-z]+))");
private static final Pattern FLAG_ALIAS_PATTERN = Pattern.compile(" -(?<name>([A-Za-z]+))");

/**
* Dummy object that indicates that flags were parsed successfully
Expand Down Expand Up @@ -125,13 +125,13 @@ private FlagArgumentParser(final @NonNull CommandFlag<?>[] flags) {
/**
* Parse command input to figure out what flag is currently being
* typed at the end of the input queue. If no flag value is being
* inputed, returns {@link Optional#empty()}.<br>
* inputted, returns {@link Optional#empty()}.<br>
* <br>
* Will consume all but the last element from the input queue.
*
* @param commandContext Command context
* @param inputQueue The input queue of arguments
* @return current flag being typed, or <i>empty()</i> if none is
* @return current flag being typed, or {@code empty()} if none is
*/
public @NonNull Optional<String> parseCurrentFlag(
final @NonNull CommandContext<@NonNull C> commandContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ public final class StandardParameters {
* Indicates that a string argument should be greedy
*/
public static final ParserParameter<Boolean> GREEDY = create("greedy", TypeToken.get(Boolean.class));
/**
* Indicates that an argument should stop parsing when encountering a potential flag.
*
* @since 1.7.0
* @see cloud.commandframework.annotations.specifier.FlagYielding
*/
public static final ParserParameter<Boolean> FLAG_YIELDING = create(
"flag_yielding",
TypeToken.get(Boolean.class)
);
/**
* Indicates that a string argument should be quoted.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
//
package cloud.commandframework.arguments.parser;

import cloud.commandframework.annotations.specifier.FlagYielding;
import cloud.commandframework.annotations.specifier.Greedy;
import cloud.commandframework.annotations.specifier.Liberal;
import cloud.commandframework.annotations.specifier.Quoted;
Expand Down Expand Up @@ -104,6 +105,10 @@ public StandardParserRegistry() {
Liberal.class,
(liberal, typeToken) -> ParserParameters.single(StandardParameters.LIBERAL, true)
);
this.<FlagYielding, String>registerAnnotationMapper(
FlagYielding.class,
(flagYielding, typeToken) -> ParserParameters.single(StandardParameters.FLAG_YIELDING, true)
);

/* Register standard types */
this.registerParserSupplier(TypeToken.get(Byte.class), options ->
Expand Down Expand Up @@ -137,10 +142,13 @@ public StandardParserRegistry() {
(double) options.get(StandardParameters.RANGE_MAX, Double.POSITIVE_INFINITY)
));
this.registerParserSupplier(TypeToken.get(Character.class), options -> new CharArgument.CharacterParser<>());
this.registerParserSupplier(TypeToken.get(String[].class), options -> new StringArrayArgument.StringArrayParser<>());
this.registerParserSupplier(TypeToken.get(String[].class), options ->
new StringArrayArgument.StringArrayParser<>(options.get(StandardParameters.FLAG_YIELDING, false))
);
/* Make this one less awful */
this.registerParserSupplier(TypeToken.get(String.class), options -> {
final boolean greedy = options.get(StandardParameters.GREEDY, false);
final boolean greedyFlagAware = options.get(StandardParameters.FLAG_YIELDING, false);
final boolean quoted = options.get(StandardParameters.QUOTED, false);
if (greedy && quoted) {
throw new IllegalArgumentException(
Expand All @@ -150,6 +158,8 @@ public StandardParserRegistry() {
final StringArgument.StringMode stringMode;
if (greedy) {
stringMode = StringArgument.StringMode.GREEDY;
} else if (greedyFlagAware) {
stringMode = StringArgument.StringMode.GREEDY_FLAG_YIELDING;
} else if (quoted) {
stringMode = StringArgument.StringMode.QUOTED;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ private StringArgument(
return of(name, StringMode.GREEDY);
}

/**
* Create a new required command argument with the 'greedy flag yielding' parsing mode
*
* @param name Argument name
* @param <C> Command sender type
* @return Created argument
* @since 1.7.0
*/
public static <C> @NonNull CommandArgument<C, String> greedyFlagYielding(final @NonNull String name) {
return of(name, StringMode.GREEDY_FLAG_YIELDING);
}

/**
* Create a new required command argument with the 'quoted' parsing mode
*
Expand All @@ -189,8 +201,14 @@ private StringArgument(

public enum StringMode {
SINGLE,
QUOTED,
GREEDY,
QUOTED
/**
* Greedy string that will consume the input until a flag is present.
*
* @since 1.7.0
*/
GREEDY_FLAG_YIELDING
}


Expand Down Expand Up @@ -224,6 +242,17 @@ private Builder(final @NonNull String name) {
return this;
}

/**
* Greedy string that will consume the input until a flag is present.
*
* @return Builder instance
* @since 1.7.0
*/
public @NonNull @This Builder<C> greedyFlagYielding() {
this.stringMode = StringMode.GREEDY_FLAG_YIELDING;
return this;
}

/**
* Set the string mode to single
*
Expand Down Expand Up @@ -274,8 +303,11 @@ private Builder(final @NonNull String name) {
}


@SuppressWarnings("UnnecessaryLambda")
public static final class StringParser<C> implements ArgumentParser<C, String> {

private static final Pattern FLAG_PATTERN = Pattern.compile("(-[A-Za-z_\\-0-9])|(--[A-Za-z_\\-0-9]*)");

private final StringMode stringMode;
private final BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider;

Expand Down Expand Up @@ -390,6 +422,13 @@ public StringParser(
break;
}

if (this.stringMode == StringMode.GREEDY_FLAG_YIELDING) {
// The pattern requires a leading space.
if (FLAG_PATTERN.matcher(string).matches()) {
break;
}
}

sj.add(string);
inputQueue.remove();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.context.CommandContext;
import io.leangen.geantyref.TypeToken;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

Expand All @@ -48,12 +50,13 @@ private StringArrayArgument(
final boolean required,
final @NonNull String name,
final @Nullable BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider,
final @NonNull ArgumentDescription defaultDescription
final @NonNull ArgumentDescription defaultDescription,
final boolean flagYielding
) {
super(
required,
name,
new StringArrayParser<>(),
new StringArrayParser<>(flagYielding),
"",
TypeToken.get(String[].class),
suggestionsProvider,
Expand All @@ -74,10 +77,35 @@ private StringArrayArgument(
final @NonNull BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider
) {
return new StringArrayArgument<>(
true,
true /* required */,
name,
suggestionsProvider,
ArgumentDescription.empty()
ArgumentDescription.empty(),
false /* flagYielding */
);
}

/**
* Create a new required string array argument
*
* @param name Argument name
* @param flagYielding Whether the parser should stop parsing when encountering a potential flag
* @param suggestionsProvider Suggestions provider
* @param <C> Command sender type
* @return Created argument
* @since 1.7.0
*/
public static <C> @NonNull StringArrayArgument<C> of(
final @NonNull String name,
final boolean flagYielding,
final @NonNull BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider
) {
return new StringArrayArgument<>(
true /* required */,
name,
suggestionsProvider,
ArgumentDescription.empty(),
flagYielding
);
}

Expand All @@ -94,10 +122,35 @@ private StringArrayArgument(
final @NonNull BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider
) {
return new StringArrayArgument<>(
false,
false /* required */,
name,
suggestionsProvider,
ArgumentDescription.empty()
ArgumentDescription.empty(),
false /* flagYielding */
);
}

/**
* Create a new optional string array argument
*
* @param name Argument name
* @param flagYielding Whether the parser should stop parsing when encountering a potential flag
* @param suggestionsProvider Suggestions provider
* @param <C> Command sender type
* @return Created argument
* @since 1.7.0
*/
public static <C> @NonNull StringArrayArgument<C> optional(
final @NonNull String name,
final boolean flagYielding,
final @NonNull BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider
) {
return new StringArrayArgument<>(
false /* required */,
name,
suggestionsProvider,
ArgumentDescription.empty(),
flagYielding
);
}

Expand All @@ -109,16 +162,54 @@ private StringArrayArgument(
*/
public static final class StringArrayParser<C> implements ArgumentParser<C, String[]> {

private static final Pattern FLAG_PATTERN = Pattern.compile("(-[A-Za-z_\\-0-9])|(--[A-Za-z_\\-0-9]*)");

private final boolean flagYielding;

/**
* Construct a new string array parser.
*/
public StringArrayParser() {
this.flagYielding = false;
}

/**
* Construct a new string array parser.
*
* @param flagYielding Whether the parser should stop parsing when encountering a potential flag
* @since 1.7.0
*/
public StringArrayParser(final boolean flagYielding) {
this.flagYielding = flagYielding;
}

@Override
public @NonNull ArgumentParseResult<String @NonNull []> parse(
final @NonNull CommandContext<@NonNull C> commandContext,
final @NonNull Queue<@NonNull String> inputQueue
) {
final String[] result = new String[inputQueue.size()];
for (int i = 0; i < result.length; i++) {
result[i] = inputQueue.remove();
if (this.flagYielding) {
final List<String> result = new LinkedList<>();
final int size = inputQueue.size();

for (int i = 0; i < size; i++) {
final String string = inputQueue.peek();
if (string == null || FLAG_PATTERN.matcher(string).matches()) {
break;
}
inputQueue.remove();

result.add(string);
}

return ArgumentParseResult.success(result.toArray(new String[0]));
} else {
final String[] result = new String[inputQueue.size()];
for (int i = 0; i < result.length; i++) {
result[i] = inputQueue.remove();
}
return ArgumentParseResult.success(result);
}
return ArgumentParseResult.success(result);
}

}
Expand Down
Loading

0 comments on commit d4ab593

Please sign in to comment.