diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 00000000..9ee09a4c --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -0,0 +1,19 @@ +name: Deploy Javadoc + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Deploy JavaDoc + uses: MathieuSoysal/Javadoc-publisher.yml@v2.4.0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + javadoc-branch: javadoc + java-version: 17 + project: gradle \ No newline at end of file diff --git a/README.md b/README.md index daaf990c..5757d432 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,3 @@ -|

Important!

Before you read the information below, please note that this project is still in development and is not ready for production use. You are free to use it (thank you if you do so), but you should be aware that the project is in a constantly changing state at the moment! | -|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - - - # Lanat Lanat is a command line argument parser for Java 17 with ease of use and high customization @@ -12,39 +7,43 @@ possibilities in mind. Here is an example of a simple argument parser definition. ```java +@Command.Define +class MyProgram { + @Argument.Define(obligatory = true, positional = true, description = "The name of the user.") + public String name; + + @Argument.Define(argType = StringArgumentType.class, description = "The surname of the user.") + public Optional surname; + + @Argument.Define(names = {"age", "a"}, description = "The age of the user.", prefix = '+') + public int age = 18; + + @InitDef + public static void beforeInit(@NotNull CommandBuildHelper cmdBuildHelper) { + // configure the argument "age" to have an argument type of + // number range and set the range to 1-100 + cmdBuildHelper., Integer>getArgument("age") + .withArgType(new NumberRangeArgumentType<>(1, 100)) + .onOk(v -> System.out.println("The age is valid!")); + } +} + class Test { public static void main(String[] args) { - final var myParser = new ArgumentParser("MyProgram") {{ - this.addArgument(Argument.create("name", ArgumentType.STRING()) - .obligatory() - .positional() // doesn't need the name to be specified - .description("The name of the user.") - ); - - this.addArgument(Argument.create("surname", ArgumentType.STRING()) - .description("The surname of the user.") - ); - - this.addArgument(Argument.create("age", ArgumentType.INTEGER()) - .defaultValue(18) - .description("The age of the user.") - .addNames("a") - .prefixChar(Argument.PrefixChar.PLUS) - ); - }}; - // example: david +a20 - final var parsedArguments = myParser.parseArgs(args); - + var myProgram = ArgumentParser.parseFromInto(MyProgram.class, CLInput.from(args)); + System.out.printf( "Welcome %s! You are %d years old.%n", - parsedArguments.get("name").get(), parsedArguments.get("age").get() + myProgram.name, myProgram.age ); - // if no surname was specified, we'll assume it is "Lewis". (Don't ask why) - System.out.println( - "The surname of the user is " + parsedArguments.get("surname").undefined("Lewis") - ); + // if no surname was specified, we'll show "none" instead + System.out.println("The surname of the user is " + myProgram.surname.orElse("none")); } } -``` \ No newline at end of file +``` + +## Documentation + +Javadoc documentation for the latest stable version is available [here](https://darvil82.github.io/Lanat/). \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index bc1eac34..7aed611b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "darvil" -version = "0.0.1-alpha" +version = "0.0.1" java.sourceCompatibility = JavaVersion.VERSION_17 repositories { @@ -21,8 +21,7 @@ repositories { } dependencies { - implementation("org.jetbrains:annotations:23.1.0") - implementation("fade:mirror:0.0.7+develop") + implementation("org.jetbrains:annotations:24.0.1") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") } diff --git a/src/main/java/lanat/Argument.java b/src/main/java/lanat/Argument.java index 327f48e2..c450963a 100644 --- a/src/main/java/lanat/Argument.java +++ b/src/main/java/lanat/Argument.java @@ -1,6 +1,7 @@ package lanat; -import lanat.argumentTypes.BooleanArgument; +import lanat.argumentTypes.BooleanArgumentType; +import lanat.argumentTypes.DummyArgumentType; import lanat.exceptions.ArgumentAlreadyExistsException; import lanat.parsing.errors.CustomError; import lanat.parsing.errors.ParseError; @@ -9,47 +10,44 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.Consumer; + /** *

Argument

*

* An Argument specifies a value that the user can introduce to the command. This value will be parsed by the specified * {@link ArgumentType} each time the Argument is used. Once finished parsing, the value may be retrieved by using * {@link ParsedArguments#get(String)} on the {@link ParsedArguments} object returned by - * {@link ArgumentParser#parseArgs(String[])}. + * {@link ArgumentParser#parse(CLInput)}. * *

- * An Argument can be created using the factory methods available, like {@link Argument#create(String...)}. + * An Argument can be created using the factory methods available, like {@link Argument#createOfBoolType(String...)}. *

*

*

Example:

* *

- * An Argument with the names "name" and "n" that will parse an integer value. There are several ways to create this - * argument. + * An Argument with the names "name" and 'n' that will parse an integer value. In order to create an Argument, you need + * to call any of the static factory methods available, like {@link Argument#createOfBoolType(String...)}. These methods + * will return an {@link ArgumentBuilder} object, which can be used to specify the Argument's properties easily. *

- *

Using the factory methods:

*
  * {@code
- *     Argument.create(ArgumentType.INTEGER(), "name", "n");
- *     Argument.create("name", ArgumentType.INTEGER())
+ *     Argument.create('n', "name", new IntegerArgumentType());
+ *     Argument.create("name", new IntegerArgumentType())
  *         .addNames("n");
  * }
  * 
* - *

Using the constructors:

- *
- * {@code
- *     new Argument<>(ArgumentType.INTEGER(), "name", "n");
- *     new Argument<>("name", ArgumentType.INTEGER())
- *         .addNames("n");
- * }
- * 
* *

Argument usage

* The argument can be used in the following ways: @@ -71,11 +69,12 @@ */ public class Argument, TInner> implements ErrorsContainer, - ErrorCallbacks>, - Resettable, - CommandUser, - MultipleNamesAndDescription> + Resettable, + CommandUser, + ArgumentGroupUser, + MultipleNamesAndDescription { /** * The type of this argument. This is the subParser that will be used to parse the value/s this argument should @@ -91,12 +90,12 @@ public class Argument, TInner> private @Nullable TInner defaultValue; - /** The Command that this Argument belongs to. This should never be null after initialization. */ + /** The Command that this Argument belongs to. This should never be {@code null} after initialization. */ private Command parentCommand; /** * The ArgumentGroup that this Argument belongs to. If this Argument does not belong to any group, this may be - * null. + * {@code null}. */ private @Nullable ArgumentGroup parentGroup; @@ -107,9 +106,9 @@ public class Argument, TInner> /** * The color that this Argument will have in places where it is displayed, such as the help message. By default, the * color will be picked from the {@link Command#colorsPool} of the parent command at - * {@link Argument#setParentCommand(Command)}. + * {@link Argument#registerToCommand(Command)}. */ - private final @NotNull ModifyRecord representationColor = new ModifyRecord<>(null); + private final @NotNull ModifyRecord representationColor = ModifyRecord.empty(); /** @@ -117,8 +116,9 @@ public class Argument, TInner> *

* The {@link PrefixChar#AUTO} prefix will be automatically set depending on the Operating System. *

+ * * @see PrefixChar#AUTO - * */ + */ public static class PrefixChar { public static final PrefixChar MINUS = new PrefixChar('-'); public static final PrefixChar PLUS = new PrefixChar('+'); @@ -133,9 +133,9 @@ public static class PrefixChar { public static final PrefixChar COLON = new PrefixChar(':'); /** - * This prefix will be automatically set depending on the Operating System. - * On Linux, it will be {@link PrefixChar#MINUS}, and on Windows, it will be {@link PrefixChar#SLASH}. - * */ + * This prefix will be automatically set depending on the Operating System. On Linux, it will be + * {@link PrefixChar#MINUS}, and on Windows, it will be {@link PrefixChar#SLASH}. + */ public static final PrefixChar AUTO = System.getProperty("os.name").toLowerCase().contains("win") ? SLASH : MINUS; @@ -150,9 +150,11 @@ private PrefixChar(char character) { * Creates a new PrefixChar with the specified non-whitespace character. *

* NOTE:
- * The constant fields of this class should be used instead of this method. Other characters - * could break compatibility with shells using special characters as prefixes, such as the | or ; characters. + * The constant fields of this class should be used instead of this method. Other characters could break + * compatibility with shells using special characters as prefixes, such as the | or ; + * characters. *

+ * * @param character the character that will be used as a prefix */ public static @NotNull PrefixChar fromCharUnsafe(char character) { @@ -162,88 +164,80 @@ private PrefixChar(char character) { } } + Argument(@NotNull Type type, @NotNull String... names) { + this.argType = type; + this.addNames(names); + } /** - * Creates an argument with the specified type and names. - * - * @param argType the type of the argument. This is the subParser that will be used to parse the value/s this - * argument should receive. - * @param names the names of the argument. See {@link Argument#addNames(String...)} for more information. + * Creates an argument builder with no type or names. + * @param the {@link ArgumentType} subclass that will parse the value passed to the argument + * @param the actual type of the value passed to the argument */ - public Argument(@NotNull Type argType, @NotNull String... names) { - this.addNames(names); - this.argType = argType; + public static , TInner> + ArgumentBuilder create() { + return new ArgumentBuilder<>(); } /** - * Creates an argument with the specified name and type. - * @param name the name of the argument. See {@link Argument#addNames(String...)} for more information. + * Creates an argument builder with the specified type and names. + * * @param argType the type of the argument. This is the subParser that will be used to parse the value/s this * argument should receive. - * */ - public Argument(@NotNull String name, @NotNull Type argType) { - this(argType, name); - } - - /** - * Creates an argument with a {@link BooleanArgument} type. * @param names the names of the argument. See {@link Argument#addNames(String...)} for more information. */ - public static Argument create(@NotNull String... names) { - return new Argument<>(ArgumentType.BOOLEAN(), names); - } - - /** Creates an argument with the specified name and type. - * @param name the name of the argument. See {@link Argument#addNames(String...)} for more information. - * @param argType the type of the argument. This is the subParser that will be used to parse the value/s this - * argument should receive. - * */ public static , TInner> - Argument create(@NotNull String name, @NotNull Type argType) { - return new Argument<>(argType, name); + ArgumentBuilder create(@NotNull Type argType, @NotNull String... names) { + return Argument.create().withNames(names).withArgType(argType); } - /** Creates an argument with the specified type and names. - * @param argType the type of the argument. This is the subParser that will be used to parse the value/s this - * argument should receive. - * @param names the names of the argument. See {@link Argument#addNames(String...)} for more information. - * */ - public static , TInner> - Argument create(@NotNull Type argType, @NotNull String... names) { - return new Argument<>(argType, names); - } - - /** Creates an argument with the specified single character name and type. + /** + * Creates an argument builder with the specified single character name and type. + * * @param name the name of the argument. See {@link Argument#addNames(String...)} for more information. * @param argType the type of the argument. This is the subParser that will be used to parse the value/s this - * */ + */ public static , TInner> - Argument create(char name, @NotNull Type argType) { - return new Argument<>(argType, String.valueOf(name)); + ArgumentBuilder create(@NotNull Type argType, char name) { + return Argument.create(argType, String.valueOf(name)); } - /** Creates an argument with the specified single character name, full name and type. + /** + * Creates an argument builder with the specified single character name, full name and type. *

* This is equivalent to calling

{@code Argument.create(charName, argType).addNames(fullName)}
* * @param charName the single character name of the argument. * @param fullName the full name of the argument. * @param argType the type of the argument. This is the subParser that will be used to parse the value/s this - * */ + */ public static , TInner> - Argument create(char charName, @NotNull String fullName, @NotNull Type argType) { - return new Argument<>(argType, String.valueOf(charName), fullName); + ArgumentBuilder create(@NotNull Type argType, char charName, @NotNull String fullName) { + return Argument.create(argType).withNames(fullName, String.valueOf(charName)); + } + + /** + * Creates an argument builder with a {@link BooleanArgumentType} type. + * + * @param names the names of the argument. See {@link Argument#addNames(String...)} for more information. + */ + public static ArgumentBuilder createOfBoolType(@NotNull String... names) { + return Argument.create(new BooleanArgumentType()).withNames(names); } /** * Marks the argument as obligatory. This means that this argument should always be used by the user. */ - public Argument obligatory() { - this.obligatory = true; - return this; + public void setObligatory(boolean obligatory) { + this.obligatory = obligatory; } + /** + * Returns {@code true} if this argument is obligatory. + * @return {@code true} if this argument is obligatory. + * @see #setObligatory(boolean) + */ public boolean isObligatory() { return this.obligatory; } @@ -256,14 +250,18 @@ public boolean isObligatory() { *
  • Note that an argument marked as positional can still be used by specifying a name. * */ - public Argument positional() { - if (this.argType.getRequiredArgValueCount().max() == 0) { + public void setPositional(boolean positional) { + if (positional && this.argType.getRequiredArgValueCount().max() == 0) { throw new IllegalArgumentException("An argument that does not accept values cannot be positional"); } - this.positional = true; - return this; + this.positional = positional; } + /** + * Returns {@code true} if this argument is positional. + * @return {@code true} if this argument is positional. + * @see #setPositional(boolean) + */ public boolean isPositional() { return this.positional; } @@ -276,13 +274,13 @@ public boolean isPositional() { * @param prefixChar the prefix that should be used for this argument. * @see PrefixChar */ - public Argument prefix(PrefixChar prefixChar) { + public void setPrefix(PrefixChar prefixChar) { this.prefixChar = prefixChar; - return this; } /** * Returns the prefix of this argument. + * * @return the prefix of this argument. */ public PrefixChar getPrefix() { @@ -294,11 +292,15 @@ public PrefixChar getPrefix() { * an argument in a command is set as obligatory, but one argument with {@link #allowUnique} was used, then the * unused obligatory argument will not throw an error. */ - public Argument allowUnique() { - this.allowUnique = true; - return this; + public void setAllowUnique(boolean allowUnique) { + this.allowUnique = allowUnique; } + /** + * Returns {@code true} if this argument has priority over other arguments, even if they are obligatory. + * @return {@code true} if this argument has priority over other arguments, even if they are obligatory. + * @see #setAllowUnique(boolean) + */ public boolean isUniqueAllowed() { return this.allowUnique; } @@ -307,11 +309,11 @@ public boolean isUniqueAllowed() { * The value that should be used if the user does not specify a value for this argument. If the argument does not * accept values, this value will be ignored. * - * @param value the value that should be used if the user does not specify a value for this argument. + * @param value the value that should be used if the user does not specify a value for this argument. If the value + * is {@code null}, then no default value will be used. */ - public Argument defaultValue(@NotNull TInner value) { + public void setDefaultValue(@Nullable TInner value) { this.defaultValue = value; - return this; } /** @@ -326,16 +328,23 @@ public Argument defaultValue(@NotNull TInner value) { * @param names the names that should be added to this argument. */ @Override - public Argument addNames(@NotNull String... names) { + public void addNames(@NotNull String... names) { Arrays.stream(names) - .map(UtlString::sanitizeName) - .forEach(newName -> { - if (this.names.contains(newName)) { - throw new IllegalArgumentException("Name '" + newName + "' is already used by this argument."); - } - this.names.add(newName); - }); - return this; + .map(UtlString::requireValidName) + .peek(n -> { + if (this.names.contains(n)) + throw new IllegalArgumentException("Name '" + n + "' is already used by this argument."); + }) + .forEach(this.names::add); + + // now let the parent command and group know that this argument has been modified. This is necessary to check + // for duplicate names + + if (this.parentCommand != null) + this.parentCommand.checkUniqueArguments(); + + if (this.parentGroup != null) + this.parentGroup.checkUniqueArguments(); } @Override @@ -343,12 +352,14 @@ public Argument addNames(@NotNull String... names) { return Collections.unmodifiableList(this.names); } - /** Sets the description of this argument. This description will be shown in the help message. + /** + * Sets the description of this argument. This description will be shown in the help message. + * * @param description the description of this argument. - * */ - public Argument description(@NotNull String description) { + */ + @Override + public void setDescription(@Nullable String description) { this.description = description; - return this; } @Override @@ -360,16 +371,18 @@ public Argument description(@NotNull String description) { * Sets the parent command of this argument. This is called when adding the Argument to a command at * {@link Command#addArgument(Argument)} */ - void setParentCommand(@NotNull Command parentCommand) { + @Override + public void registerToCommand(@NotNull Command parentCommand) { if (this.parentCommand != null) { throw new ArgumentAlreadyExistsException(this, this.parentCommand); } + this.parentCommand = parentCommand; this.representationColor.setIfNotModified(parentCommand.colorsPool.next()); } @Override - public @NotNull Command getParentCommand() { + public Command getParentCommand() { return this.parentCommand; } @@ -377,23 +390,28 @@ void setParentCommand(@NotNull Command parentCommand) { * Sets the parent group of this argument. This is called when adding the Argument to a group at * {@link ArgumentGroup#addArgument(Argument)} */ - void setParentGroup(@NotNull ArgumentGroup parentGroup) { + @Override + public void registerToGroup(@NotNull ArgumentGroup parentGroup) { if (this.parentGroup != null) { throw new ArgumentAlreadyExistsException(this, this.parentGroup); } + this.parentGroup = parentGroup; } /** - * Returns the {@link ArgumentGroup} that contains this argument, or null if it does not have one. - * @return the parent group of this argument, or null if it does not have one. + * Returns the {@link ArgumentGroup} that contains this argument, or {@code null} if it does not have one. + * + * @return the parent group of this argument, or {@code null} if it does not have one. */ + @Override public @Nullable ArgumentGroup getParentGroup() { return this.parentGroup; } /** * The number of times this argument has been used in a command during parsing. + * * @return the number of times this argument has been used in a command. */ public short getUsageCount() { @@ -402,6 +420,7 @@ public short getUsageCount() { /** * The color that will be used to represent this argument in the help message. + * * @return the color that will be used to represent this argument in the help message. */ public @NotNull Color getRepresentationColor() { @@ -410,6 +429,7 @@ public short getUsageCount() { /** * Sets the color that will be used to represent this argument in the help message. + * * @param color the color that will be used to represent this argument in the help message. */ public void setRepresentationColor(@NotNull Color color) { @@ -418,50 +438,23 @@ public void setRepresentationColor(@NotNull Color color) { /** - * Returns true if this argument is the help argument of its parent command. - * This just checks if the argument's name is "help" and if it is marked with {@link #allowUnique()}. - * @return true if this argument is the help argument of its parent command. + * Returns {@code true} if this argument is the help argument of its parent command. This just checks if the + * argument's name is "help" and if it is marked with {@link #setAllowUnique(boolean)}. + * + * @return {@code true} if this argument is the help argument of its parent command. */ public boolean isHelpArgument() { return this.getName().equals("help") && this.isUniqueAllowed(); } - /** - * Specify a function that will be called with the value introduced by the user. - *

    - * By default this callback is called only if all commands succeed, but you can change this behavior with - * {@link Command#invokeCallbacksWhen(CallbacksInvocationOption)} - *

    - * @param callback the function that will be called with the value introduced by the user. - */ - public Argument onOk(@NotNull Consumer<@NotNull TInner> callback) { - this.setOnCorrectCallback(callback); - return this; - } - - /** - * Specify a function that will be called if an error occurs when parsing this argument. - *

    - * Note that this callback is only called if the error was dispatched by this argument's type. That - * is, - * if the argument, for example, is obligatory, and the user does not specify a value, an error will be thrown, but - * this callback will not be called, as the error was not dispatched by this argument's type. - *

    - * @param callback the function that will be called if an error occurs when parsing this argument. - */ - public Argument onErr(@NotNull Consumer<@NotNull Argument> callback) { - this.setOnErrorCallback(callback); - return this; - } - /** * Pass the specified values array to the argument type to parse it. * - * @param values The values array that should be parsed. * @param tokenIndex This is the global index of the token that is currently being parsed. Used when dispatching * errors. + * @param values The value array that should be parsed. */ - public void parseValues(@NotNull String @NotNull [] values, short tokenIndex) { + public void parseValues(short tokenIndex, @NotNull String... values) { // check if the argument was used more times than it should if (++this.argType.usageCount > this.argType.getRequiredUsageCount().max()) { this.parentCommand.getParser() @@ -472,20 +465,13 @@ public void parseValues(@NotNull String @NotNull [] values, short tokenIndex) { return; } - this.argType.setLastTokenIndex(tokenIndex); - this.argType.parseAndUpdateValue(values); - } - - /** - * {@link #parseValues(String[], short)} but passes in an empty values array to parse. - */ - public void parseValues(short tokenIndex) { - this.parseValues(new String[0], tokenIndex); + this.argType.parseAndUpdateValue(tokenIndex, values); } /** - * This method is called when the command is finished parsing. And should only ever be called once - * (per parse). + * This method is called when the command is finished parsing. And should only ever be called once (per + * parse). + * * @return the final value parsed by the argument type, or the default value if the argument was not used. */ public @Nullable TInner finishParsing() { @@ -494,7 +480,7 @@ public void parseValues(short tokenIndex) { /* no, | is not a typo. We don't want the OR operator to short-circuit, we want all of them to be evaluated * because the methods have side effects (they add errors to the parser) */ - TInner returnValue = (finalValue == null | !this.finishParsingCheckExclusivity() | !this.finishParsingCheckUsageCount()) + TInner returnValue = (finalValue == null | !this.finishParsing$checkExclusivity() | !this.finishParsing$checkUsageCount()) ? defaultValue : finalValue; @@ -507,9 +493,10 @@ public void parseValues(short tokenIndex) { /** * Checks if the argument was used the correct amount of times. - * @return true if the argument was used the correct amount of times. + * + * @return {@code true} if the argument was used the correct amount of times. */ - private boolean finishParsingCheckUsageCount() { + private boolean finishParsing$checkUsageCount() { if (this.getUsageCount() == 0) { if (this.obligatory && !this.parentCommand.uniqueArgumentReceivedValue()) { this.parentCommand.getParser().addError( @@ -517,7 +504,7 @@ private boolean finishParsingCheckUsageCount() { ); return false; } - // make sure that the argument was used the minimum amount of times specified + // make sure that the argument was used the minimum amount of times specified } else if (this.argType.usageCount < this.argType.getRequiredUsageCount().min()) { this.parentCommand.getParser() .addError(ParseError.ParseErrorType.ARG_INCORRECT_USAGES_COUNT, this, 0); @@ -529,9 +516,10 @@ private boolean finishParsingCheckUsageCount() { /** * Checks if the argument is part of an exclusive group, and if so, checks if there is any violation of exclusivity * in the group hierarchy. - * @return true if there is no violation of exclusivity in the group hierarchy. + * + * @return {@code true} if there is no violation of exclusivity in the group hierarchy. */ - private boolean finishParsingCheckExclusivity() { + private boolean finishParsing$checkExclusivity() { // check if the parent group of this argument is exclusive, and if so, check if any other argument in it has been used if (this.parentGroup == null || this.getUsageCount() == 0) return true; @@ -542,7 +530,7 @@ private boolean finishParsingCheckExclusivity() { new ParseError( ParseError.ParseErrorType.MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED, this.argType.getLastTokenIndex(), - this, this.argType.getLastReceivedValueCount() + this, this.argType.getLastReceivedValuesNum() ) {{ this.setArgumentGroup(exclusivityResult); @@ -554,11 +542,12 @@ private boolean finishParsingCheckExclusivity() { /** * Checks if this argument matches the given name, including the prefix. *

    - * For example, if the prefix is '-' and the argument has the name "help", this method will - * return true if the name is "--help". + * For example, if the prefix is '-' and the argument has the name "help", this method + * will return {@code true} if the name is "--help". *

    + * * @param name the name to check - * @return true if the name matches, false otherwise. + * @return {@code true} if the name matches, {@code false} otherwise. */ public boolean checkMatch(@NotNull String name) { final char prefixChar = this.getPrefix().character; @@ -568,9 +557,10 @@ public boolean checkMatch(@NotNull String name) { /** * Checks if this argument matches the given single character name. - * @see #checkMatch(String) + * * @param name the name to check - * @return true if the name matches, false otherwise. + * @return {@code true} if the name matches, {@code false} otherwise. + * @see #checkMatch(String) */ public boolean checkMatch(char name) { return this.hasName(Character.toString(name)); @@ -597,22 +587,26 @@ void invokeCallbacks(@Nullable Object okValue) { } /** - * Returns true if the argument specified by the given name is equal to this argument. + * Returns {@code true} if the argument specified by the given name is equal to this argument. *

    * Equality is determined by the argument's name and the command it belongs to. *

    + * * @param obj the argument to compare to - * @return true if the argument specified by the given name is equal to this argument + * @return {@code true} if the argument specified by the given name is equal to this argument */ - public boolean equals(@NotNull Argument obj) { - return Command.equalsByNamesAndParentCmd(this, obj); + @Override + public boolean equals(@NotNull Object obj) { + if (obj instanceof Argument arg) + return UtlMisc.equalsByNamesAndParentCmd(this, arg); + return false; } /** * Compares two arguments by the synopsis view priority order. *

    * Order: - * Positional > Obligatory > Optional. + * Allows Unique > Positional > Obligatory > Optional. *

    * * @param first the first argument to compare @@ -621,7 +615,8 @@ public boolean equals(@NotNull Argument obj) { * before the first. */ public static int compareByPriority(@NotNull Argument first, @NotNull Argument second) { - return new Comparator>() + return new MultiComparator>() + .addPredicate(Argument::isUniqueAllowed, 2) .addPredicate(Argument::isPositional, 1) .addPredicate(Argument::isObligatory) .compare(first, second); @@ -629,6 +624,7 @@ public static int compareByPriority(@NotNull Argument first, @NotNull Argu /** * Sorts the given list of arguments by the synopsis view priority order. + * * @param args the arguments to sort * @return the sorted list * @see #compareByPriority(Argument, Argument) @@ -653,6 +649,38 @@ public void resetState() { ); } + /** + * Used in {@link CommandTemplate}s to specify the properties of an argument belonging to the command. + * @see CommandTemplate + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Define { + /** @see Argument#addNames(String...) */ + String[] names() default { }; + + /** @see Argument#setDescription(String) */ + String description() default ""; + + /** @see ArgumentBuilder#withArgType(ArgumentType) */ + Class> argType() default DummyArgumentType.class; + + /** + * Specifies the prefix character for this argument. This uses {@link PrefixChar#fromCharUnsafe(char)}. + * @see Argument#setPrefix(PrefixChar) + * */ + char prefix() default '-'; + + /** @see Argument#setObligatory(boolean) */ + boolean obligatory() default false; + + /** @see Argument#setPositional(boolean) */ + boolean positional() default false; + + /** @see Argument#setAllowUnique(boolean) */ + boolean allowsUnique() default false; + } + // ------------------------------------------------ Error Handling ------------------------------------------------ // just act as a proxy to the type error handling @@ -697,13 +725,33 @@ public void setMinimumExitErrorLevel(@NotNull ErrorLevel level) { return this.argType.getMinimumExitErrorLevel(); } + /** + * Specify a function that will be called if an error occurs when parsing this argument. + *

    + * Note that this callback is only called if the error was dispatched by this argument's type. + * That + * is, if the argument, for example, is obligatory, and the user does not specify a value, an error will be + * thrown, but this callback will not be called, as the error was not dispatched by this argument's type. + *

    + * + * @param callback the function that will be called if an error occurs when parsing this argument. + */ @Override - public void setOnErrorCallback(@NotNull Consumer<@NotNull Argument> callback) { + public void setOnErrorCallback(@Nullable Consumer<@NotNull Argument> callback) { this.onErrorCallback = callback; } + /** + * Specify a function that will be called with the value introduced by the user. + *

    + * By default this callback is called only if all commands succeed, but you can change this behavior with + * {@link Command#setCallbackInvocationOption(CallbacksInvocationOption)} + *

    + * + * @param callback the function that will be called with the value introduced by the user. + */ @Override - public void setOnCorrectCallback(@NotNull Consumer<@NotNull TInner> callback) { + public void setOnOkCallback(@Nullable Consumer<@NotNull TInner> callback) { this.onCorrectCallback = callback; } diff --git a/src/main/java/lanat/ArgumentAdder.java b/src/main/java/lanat/ArgumentAdder.java index 830ba7c6..dcfc52bd 100644 --- a/src/main/java/lanat/ArgumentAdder.java +++ b/src/main/java/lanat/ArgumentAdder.java @@ -1,16 +1,77 @@ package lanat; +import lanat.exceptions.ArgumentAlreadyExistsException; +import lanat.exceptions.ArgumentNotFoundException; +import lanat.utils.UtlMisc; import org.jetbrains.annotations.NotNull; import java.util.List; -public interface ArgumentAdder { +/** + * An interface for objects that can add {@link Argument}s to themselves. + */ +public interface ArgumentAdder extends NamedWithDescription { /** - * Inserts an argument for this command to be parsed. + * Inserts an argument into this container. * * @param argument the argument to be inserted + * @param the type of the argument + * @param the type of the inner value of the argument */ , TInner> void addArgument(@NotNull Argument argument); + /** + * Inserts an argument into this container. This is a convenience method for {@link #addArgument(Argument)}. + * It is equivalent to {@code addArgument(argument.build())}. + * @param argument the argument to be inserted + * @param the type of the argument + * @param the type of the inner value of the argument + */ + default , TInner> + void addArgument(@NotNull ArgumentBuilder argument) { + this.addArgument(argument.build()); + } + + /** + * Returns a list of the arguments in this container. + * @return an immutable list of the arguments in this container + */ @NotNull List<@NotNull Argument> getArguments(); + + /** + * Checks that all the arguments in this container have unique names. + * @throws ArgumentAlreadyExistsException if there are two arguments with the same name + */ + default void checkUniqueArguments() { + UtlMisc.requireUniqueElements(this.getArguments(), a -> new ArgumentAlreadyExistsException(a, this)); + } + + /** + * Checks if this container has an argument with the given name. + * @param name the name of the argument + * @return {@code true} if this container has an argument with the given name, {@code false} otherwise + */ + default boolean hasArgument(@NotNull String name) { + for (final var argument : this.getArguments()) { + if (argument.hasName(name)) { + return true; + } + } + return false; + } + + /** + * Returns the argument with the given name. + * @param name the name of the argument + * @return the argument with the given name + * @throws ArgumentNotFoundException if there is no argument with the given name + */ + default @NotNull Argument getArgument(@NotNull String name) { + for (final var argument : this.getArguments()) { + if (argument.hasName(name)) { + return argument; + } + } + throw new ArgumentNotFoundException(name); + } } diff --git a/src/main/java/lanat/ArgumentBuilder.java b/src/main/java/lanat/ArgumentBuilder.java new file mode 100644 index 00000000..fccc13e2 --- /dev/null +++ b/src/main/java/lanat/ArgumentBuilder.java @@ -0,0 +1,241 @@ +package lanat; + +import lanat.argumentTypes.DummyArgumentType; +import lanat.utils.UtlReflection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Provides easy ways to build an {@link Argument}. + * @param the {@link ArgumentType} subclass that will parse the value passed to the argument + * @param the actual type of the value passed to the argument + */ +public class ArgumentBuilder, TInner> { + private @NotNull String @Nullable [] names; + private @Nullable String description; + private @Nullable Type argType; + private boolean obligatory = false, + positional = false, + allowUnique = false; + private @Nullable TInner defaultValue; + private @Nullable Consumer<@NotNull Argument> onErrorCallback; + private @Nullable Consumer<@NotNull TInner> onCorrectCallback; + private @Nullable Argument.PrefixChar prefixChar = Argument.PrefixChar.defaultPrefix; + + ArgumentBuilder() {} + + + /** + * Returns an {@link ArgumentType} instance based on the specified field. If the annotation specifies a type, + * it will be used. Otherwise, the type will be inferred from the field type. If the type cannot be inferred, + * null will be returned. + * Note: Expects the field to be annotated with {@link Argument.Define} + * + * @param field the field that will be used to build the argument + * @return the built argument type + */ + public static @Nullable ArgumentType getArgumentTypeFromField(@NotNull Field field) { + final var annotation = field.getAnnotation(Argument.Define.class); + assert annotation != null : "The field must have an Argument.Define annotation."; + + // if the type is not a dummy type (it was specified on the annotation), instantiate it and return it + if (annotation.argType() != DummyArgumentType.class) + return UtlReflection.instantiate(annotation.argType()); + + // try to infer the type from the field type + var argTypeMap = ArgumentType.getTypeInfer(field.getType()); + + // if the type was not found, return null + return argTypeMap == null ? null : UtlReflection.instantiate(argTypeMap); + } + + /** + * Builds an {@link Argument} from the specified field annotated with {@link Argument.Define}. + * Note that this doesn't set the argument type. + * + * @param field the field that will be used to build the argument + * @param the {@link ArgumentType} subclass that will parse the value passed to the argument + * @param the actual type of the value passed to the argument + * @return the built argument + */ + public static @NotNull , TInner> + ArgumentBuilder fromField(@NotNull Field field) { + final var annotation = field.getAnnotation(Argument.Define.class); + + if (annotation == null) + throw new IllegalArgumentException("The field must have an Argument.Define annotation."); + + final var argumentBuilder = new ArgumentBuilder() + .withNames(ArgumentBuilder.getTemplateFieldNames(field)); + + argumentBuilder.withPrefix(Argument.PrefixChar.fromCharUnsafe(annotation.prefix())); + if (!annotation.description().isEmpty()) argumentBuilder.withDescription(annotation.description()); + if (annotation.obligatory()) argumentBuilder.obligatory(); + if (annotation.positional()) argumentBuilder.positional(); + if (annotation.allowsUnique()) argumentBuilder.allowsUnique(); + + return argumentBuilder; + } + + /** + * Builds an {@link Argument} from the specified field name in the specified {@link CommandTemplate} subclass. + * + * @param templateClass the {@link CommandTemplate} subclass that contains the field + * @param fieldName the name of the field that will be used to build the argument + * @param the {@link ArgumentType} subclass that will parse the value passed to the argument + * @param the actual type of the value passed to the argument + * @return the built argument + */ + public static @NotNull , TInner> + ArgumentBuilder fromField( + @NotNull Class templateClass, + @NotNull String fieldName + ) + { + return ArgumentBuilder.fromField(Stream.of(templateClass.getDeclaredFields()) + .filter(f -> f.isAnnotationPresent(Argument.Define.class)) + .filter(f -> f.getName().equals(fieldName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("The field " + fieldName + " does not exist in " + + "the template class " + templateClass.getSimpleName()) + ) + ); + } + + /** + * Returns the names of the argument, either the ones specified in the {@link Argument.Define} annotation or the + * field name if the names are empty. + * Note: Expects the field to be annotated with {@link Argument.Define} + * + * @param field the field that will be used to get the names. It must have an {@link Argument.Define} annotation. + * @return the names of the argument + */ + static @NotNull String[] getTemplateFieldNames(@NotNull Field field) { + final var annotation = field.getAnnotation(Argument.Define.class); + assert annotation != null : "The field must have an Argument.Define annotation."; + + final var annotationNames = annotation.names(); + + // if the names are empty, use the field name + return annotationNames.length == 0 + ? new String[] { field.getName() } + : annotationNames; + } + + /** + * Returns {@code true} if the specified name is one of the names of the argument. + * + * @param name the name that will be checked + * @return {@code true} if the specified name is one of the names of the argument + */ + boolean hasName(@NotNull String name) { + return this.names != null && Arrays.asList(this.names).contains(name); + } + + /** @see Argument#setDescription(String) */ + public ArgumentBuilder withDescription(@NotNull String description) { + this.description = description; + return this; + } + + /** @see Argument#setObligatory(boolean) */ + public ArgumentBuilder obligatory() { + this.obligatory = true; + return this; + } + + /** @see Argument#setPositional(boolean) */ + public ArgumentBuilder positional() { + this.positional = true; + return this; + } + + /** @see Argument#setAllowUnique(boolean) */ + public ArgumentBuilder allowsUnique() { + this.allowUnique = true; + return this; + } + + /** @see Argument#setDefaultValue(Object) */ + public ArgumentBuilder withDefaultValue(@NotNull TInner defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** @see Argument#setOnOkCallback(Consumer) */ + public ArgumentBuilder onOk(@NotNull Consumer callback) { + this.onCorrectCallback = callback; + return this; + } + + /** @see Argument#setOnErrorCallback(Consumer) */ + public ArgumentBuilder onErr(@NotNull Consumer> callback) { + this.onErrorCallback = callback; + return this; + } + + /** @see Argument#setPrefix(Argument.PrefixChar) */ + public ArgumentBuilder withPrefix(@NotNull Argument.PrefixChar prefixChar) { + this.prefixChar = prefixChar; + return this; + } + + /** @see Argument#addNames(String...) */ + public ArgumentBuilder withNames(@NotNull String... names) { + this.names = names; + return this; + } + + /** + * The Argument Type is the class that will be used to parse the argument value. It handles the conversion from the + * input string to the desired type. + * + * @see ArgumentType + * @see Argument#argType + */ + public ArgumentBuilder withArgType(@NotNull Type argType) { + this.argType = argType; + return this; + } + + /** + * Sets the argument type from the specified field. If the argument type is already set, this method does nothing. + */ + @SuppressWarnings("unchecked") + void setArgTypeFromField(@NotNull Field field) { + // if the argType is already set, don't change it + if (this.argType != null) return; + + var argType = ArgumentBuilder.getArgumentTypeFromField(field); + if (argType != null) this.withArgType((Type)argType); + } + + /** + * Builds the argument. + * + * @return the built argument + */ + public Argument build() { + if (this.names == null || this.names.length == 0) + throw new IllegalStateException("The argument must have at least one name."); + + if (this.argType == null) + throw new IllegalStateException("The argument must have a type defined."); + + return new Argument<>(this.argType, this.names) {{ + this.setDescription(ArgumentBuilder.this.description); + this.setObligatory(ArgumentBuilder.this.obligatory); + this.setPositional(ArgumentBuilder.this.positional); + this.setAllowUnique(ArgumentBuilder.this.allowUnique); + this.setDefaultValue(ArgumentBuilder.this.defaultValue); + this.setPrefix(ArgumentBuilder.this.prefixChar); + this.setOnErrorCallback(ArgumentBuilder.this.onErrorCallback); + this.setOnOkCallback(ArgumentBuilder.this.onCorrectCallback); + }}; + } +} diff --git a/src/main/java/lanat/ArgumentGroup.java b/src/main/java/lanat/ArgumentGroup.java index 3641b438..fcc230a8 100644 --- a/src/main/java/lanat/ArgumentGroup.java +++ b/src/main/java/lanat/ArgumentGroup.java @@ -10,17 +10,64 @@ import java.util.Collections; import java.util.List; +/** + *

    Argument Group

    + *

    + * Represents a group of arguments. This is used to group arguments together, and to set exclusivity between them. + * When a group is exclusive, it means that only one argument in it can be used at a time. + *

    + * Groups can also be used to simply indicate arguments that are related to each other, and to set a description + * to this relation. This is useful for the help message representation. + *

    + * Groups can be nested, meaning that a group can contain other groups. This is useful for setting exclusivity between + * arguments that are in different groups. For example, given the following group tree: + *

    + *            +-----------------------+
    + *            |  Group 1 (exclusive)  |
    + *            |-----------------------|
    + *            | Argument 1            |
    + *            +-----------------------+
    + *                       |
    + *          +---------------------------+
    + *          |                           |
    + *  +---------------+      +-------------------------+
    + *  |    Group 2    |      |   Group 3 (exclusive)   |
    + *  |---------------|      |-------------------------|
    + *  | Argument 2.1  |      | Argument 3.1            |
    + *  | Argument 2.2  |      | Argument 3.2            |
    + *  +---------------+      +-------------------------+
    + * 
    + *
      + *
    • + * If {@code Argument 1} is used, then none of the arguments in the child groups can be used, because {@code Group 1} + * is exclusive. + *
    • + *
    • + * If {@code Argument 3.1} is used, then none of the arguments in the rest of the tree can be used, because + * both {@code Group 3} and its parent {@code Group 1} are exclusive. + *
    • + *
    • + * If {@code Argument 2.1} is used, {@code Argument 2.2} can still be used, because {@code Group 2} is not exclusive. + * No other arguments in the tree can be used though. + *
    • + *
    + */ public class ArgumentGroup implements ArgumentAdder, - ArgumentGroupAdder, - Resettable, - CommandUser, - NamedWithDescription, - ParentElementGetter + ArgumentGroupAdder, + CommandUser, + ArgumentGroupUser, + Resettable, + NamedWithDescription, + ParentElementGetter { - public final @NotNull String name; - public @Nullable String description; + private final @NotNull String name; + private @Nullable String description; + + /** The parent command of this group. This is set when the group is added to a command. */ private Command parentCommand; + + /** The parent group of this group. This is set when the group is added to another group. */ private @Nullable ArgumentGroup parentGroup; /** @@ -41,17 +88,27 @@ public class ArgumentGroup private boolean isExclusive = false; /** - * When set to true, indicates that one argument in this group has been used. This is used when later checking for - * exclusivity in the groups tree at {@link ArgumentGroup#checkExclusivity(ArgumentGroup)} + * When set to {@code true}, indicates that one argument in this group has been used. This is used when later + * checking for exclusivity in the groups tree at {@link ArgumentGroup#checkExclusivity(ArgumentGroup)} */ private boolean argumentUsed = false; + /** + * Creates a new Argument Group with the given name and description. + * The name and descriptions are basically only used for the help message. + * @param name The name of the group. Must be a unique name among all groups in the same command. + * @param description The description of the group. + */ public ArgumentGroup(@NotNull String name, @Nullable String description) { - this.name = UtlString.sanitizeName(name); + this.name = UtlString.requireValidName(name); this.description = description; } + /** + * Creates a new Argument Group with the given name and no description. + * @param name The name of the group. Must be a unique name among all groups in the same command. + */ public ArgumentGroup(@NotNull String name) { this(name, null); } @@ -60,8 +117,9 @@ public ArgumentGroup(@NotNull String name) { @Override public , TInner> void addArgument(@NotNull Argument argument) { + argument.registerToGroup(this); this.arguments.add(argument); - argument.setParentGroup(this); + this.checkUniqueArguments(); } @Override @@ -70,64 +128,87 @@ void addArgument(@NotNull Argument argument) { } @Override - public @NotNull List getSubGroups() { + public @NotNull List getGroups() { return Collections.unmodifiableList(this.subGroups); } @Override public void addGroup(@NotNull ArgumentGroup group) { - if (group.parentGroup != null) { - throw new ArgumentGroupAlreadyExistsException(group, group.parentGroup); + if (group == this) { + throw new IllegalArgumentException("A group cannot be added to itself"); } - if (this.subGroups.stream().anyMatch(g -> g.name.equals(group.name))) { - throw new ArgumentGroupAlreadyExistsException(group, group); - } - - group.parentGroup = this; - group.parentCommand = this.parentCommand; + group.registerToGroup(this); this.subGroups.add(group); + this.checkUniqueGroups(); } - /** - * Sets this group to be exclusive, meaning that only one argument in it can be used. - */ - public void exclusive() { - this.isExclusive = true; - } + @Override + public void registerToGroup(@NotNull ArgumentGroup parentGroup) { + if (this.parentGroup != null) { + throw new ArgumentGroupAlreadyExistsException(this, this.parentGroup); + } - public boolean isExclusive() { - return this.isExclusive; + this.parentGroup = parentGroup; + this.parentCommand = parentGroup.parentCommand; } /** * Sets this group's parent command, and also passes all its arguments to the command. */ - void registerGroup(@NotNull Command parentCommand) { + @Override + public void registerToCommand(@NotNull Command parentCommand) { if (this.parentCommand != null) { throw new ArgumentGroupAlreadyExistsException(this, this.parentCommand); } this.parentCommand = parentCommand; - for (var argument : this.arguments) { - parentCommand.addArgument(argument); - } - this.subGroups.forEach(g -> g.registerGroup(parentCommand)); + + // if the argument already has a parent command, it means that it was added to the command before this group was + // added to it, so we don't need to add it again (it would cause an exception) + this.arguments.stream() + .filter(a -> a.getParentCommand() == null) + .forEach(parentCommand::addArgument); + + this.subGroups.forEach(g -> g.registerToCommand(parentCommand)); } @Override - public @NotNull Command getParentCommand() { + public Command getParentCommand() { return this.parentCommand; } + @Override + public @Nullable ArgumentGroup getParentGroup() { + return this.parentGroup; + } + + /** + * Sets this group to be exclusive, meaning that only one argument in it can be used at a time. + * @see ArgumentGroup#isExclusive() + */ + public void setExclusive(boolean isExclusive) { + this.isExclusive = isExclusive; + } + + /** + * Returns {@code true} if this group is exclusive. + * @return {@code true} if this group is exclusive. + * @see ArgumentGroup#setExclusive(boolean) + */ + public boolean isExclusive() { + return this.isExclusive; + } + /** - * Checks if there is any violation of exclusivity in this group's tree, from this group to the root. - * This is done by checking if this or any of the group's siblings have been used (except for the childCallee, which is - * the group that called this method). If none of them have been used, the parent group is checked, and so on. + * Checks if there is any violation of exclusivity in this group's tree, from this group to the root. This is done + * by checking if this or any of the group's siblings have been used (except for the childCallee, which is the group + * that called this method). If none of them have been used, the parent group is checked, and so on. + * * @param childCallee The group that called this method. This is used to avoid checking the group that called this - * method, because it is the one that is being checked for exclusivity. This can be null if this is the - * first call to this method. - * @return The group that caused the violation, or null if there is no violation. + * method, because it is the one that is being checked for exclusivity. This can be {@code null} if this is + * the first call to this method. + * @return The group that caused the violation, or {@code null} if there is no violation. */ @Nullable ArgumentGroup checkExclusivity(@Nullable ArgumentGroup childCallee) { if ( @@ -143,15 +224,24 @@ void registerGroup(@NotNull Command parentCommand) { return null; } + /** + * Returns {@code true} if this group has no arguments and no subgroups. + * @return {@code true} if this group has no arguments and no subgroups. + */ public boolean isEmpty() { return this.arguments.isEmpty() && this.subGroups.isEmpty(); } + /** + * Marks that an argument in this group has been used. This is used to later check for exclusivity. + * This also marks the parent group as used, and so on until reaching the root of the groups tree, thus marking the + * path of the used argument. + */ void setArgUsed() { this.argumentUsed = true; - // set argUsed to true on all parents until reaching the groups root + // set argUsed to {@code true} on all parents until reaching the groups root if (this.parentGroup != null) this.parentGroup.setArgUsed(); } @@ -171,7 +261,7 @@ public void resetState() { public void setDescription(@NotNull String description) { this.description = description; } - + @Override public @Nullable String getDescription() { return this.description; @@ -181,6 +271,13 @@ public void setDescription(@NotNull String description) { public ArgumentGroup getParent() { return this.parentGroup; } + + @Override + public boolean equals(@NotNull Object obj) { + if (obj instanceof ArgumentGroup group) + return this.parentCommand == group.parentCommand && this.name.equals(group.name); + return false; + } } diff --git a/src/main/java/lanat/ArgumentGroupAdder.java b/src/main/java/lanat/ArgumentGroupAdder.java index 8e813186..f9cda9d8 100644 --- a/src/main/java/lanat/ArgumentGroupAdder.java +++ b/src/main/java/lanat/ArgumentGroupAdder.java @@ -1,14 +1,62 @@ package lanat; +import lanat.exceptions.ArgumentGroupAlreadyExistsException; +import lanat.exceptions.ArgumentGroupNotFoundException; +import lanat.utils.UtlMisc; import org.jetbrains.annotations.NotNull; import java.util.List; -public interface ArgumentGroupAdder { +/** + * An interface for objects that can add {@link ArgumentGroup}s to themselves. + */ +public interface ArgumentGroupAdder extends NamedWithDescription { /** - * Adds an argument group to this element. + * Adds an argument group to this container. + * @param group the argument group to be added */ void addGroup(@NotNull ArgumentGroup group); - @NotNull List<@NotNull ArgumentGroup> getSubGroups(); + /** + * Returns a list of the argument groups in this container. + * @return an immutable list of the argument groups in this container + */ + @NotNull List<@NotNull ArgumentGroup> getGroups(); + + /** + * Checks that all the argument groups in this container have unique names. + * @throws ArgumentGroupAlreadyExistsException if there are two argument groups with the same name + */ + default void checkUniqueGroups() { + UtlMisc.requireUniqueElements(this.getGroups(), g -> new ArgumentGroupAlreadyExistsException(g, this)); + } + + /** + * Checks if this container has an argument group with the given name. + * @param name the name of the argument group + * @return {@code true} if this container has an argument group with the given name, {@code false} otherwise + */ + default boolean hasGroup(@NotNull String name) { + for (final var group : this.getGroups()) { + if (group.getName().equals(name)) { + return true; + } + } + return false; + } + + /** + * Returns the argument group with the given name. + * @param name the name of the argument group + * @return the argument group with the given name + * @throws ArgumentGroupNotFoundException if there is no argument group with the given name + */ + default @NotNull ArgumentGroup getGroup(@NotNull String name) { + for (final var group : this.getGroups()) { + if (group.getName().equals(name)) { + return group; + } + } + throw new ArgumentGroupNotFoundException(name); + } } diff --git a/src/main/java/lanat/ArgumentGroupUser.java b/src/main/java/lanat/ArgumentGroupUser.java new file mode 100644 index 00000000..d80474de --- /dev/null +++ b/src/main/java/lanat/ArgumentGroupUser.java @@ -0,0 +1,21 @@ +package lanat; + +import org.jetbrains.annotations.NotNull; + +/** + * This interface is used for objects that belong to an {@link ArgumentGroup}. + */ +public interface ArgumentGroupUser { + /** + * Gets the {@link ArgumentGroup} object that this object belongs to. + * + * @return The parent group of this object. + */ + ArgumentGroup getParentGroup(); + + /** + * Sets the parent group of this object. + * @param parentGroup the parent group to set + */ + void registerToGroup(@NotNull ArgumentGroup parentGroup); +} diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java index 583e76e1..94083be7 100644 --- a/src/main/java/lanat/ArgumentParser.java +++ b/src/main/java/lanat/ArgumentParser.java @@ -1,70 +1,172 @@ package lanat; - +import lanat.exceptions.CommandTemplateException; +import lanat.exceptions.IncompatibleCommandTemplateType; import lanat.parsing.TokenType; import lanat.parsing.errors.ErrorHandler; -import lanat.utils.Pair; +import lanat.utils.UtlReflection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + *

    Argument Parser

    + *

    + * Provides the ability to parse a command line input and later gather the values of the parsed arguments. + */ public class ArgumentParser extends Command { private boolean isParsed = false; private @Nullable String license; private @Nullable String version; + /** + * Creates a new command with the given name and description. + * @param programName The name of the command. This is the name the user will use to indicate that they want to use this + * command. + * @param description The description of the command. + * @see #setDescription(String) + */ public ArgumentParser(@NotNull String programName, @Nullable String description) { super(programName, description); } + /** + * Creates a new command with the given name and no description. This is the name the user will use to + * indicate that they want to use this command. + * @param programName The name of the command. + */ public ArgumentParser(@NotNull String programName) { this(programName, null); } + /** + * Creates a new command based on the given {@link CommandTemplate}. This does not take Sub-Commands into account. + * If you want to add Sub-Commands, use {@link #from(Class)} instead. + * @param templateClass The class of the template to use. + */ + public ArgumentParser(@NotNull Class templateClass) { + super(templateClass); + } /** - * {@link ArgumentParser#parseArgs(String)} + * Constructs a new {@link ArgumentParser} based on the given {@link CommandTemplate}, taking Sub-Commands into + * account. + * @param templateClass The class of the {@link CommandTemplate} to use. + * @return A new {@link ArgumentParser} based on the given {@link CommandTemplate}. + * @see CommandTemplate */ - public @NotNull ParsedArgumentsRoot parseArgs(@NotNull String @NotNull [] args) { - // if we receive the classic args array, just join it back - return this.parseArgs(String.join(" ", args)); + public static ArgumentParser from(@NotNull Class templateClass) { + final var argParser = new ArgumentParser(templateClass); + + // add all commands recursively + ArgumentParser.from$setCommands(templateClass, argParser); + + return argParser; } /** - * Parses the given command line arguments and returns a {@link ParsedArguments} object. + * Constructs a new {@link ArgumentParser} based on the given {@link CommandTemplate}, parses the given input, and + * populates the template with the parsed values. + *

    + * This is basically a shortcut for the following code: + *

    {@code
    +	 * new ArgumentParser(clazz).parse(input).into(clazz);
    +	 * }
    + *

    Example:

    + * This code: + *
    {@code
    +	 * MyTemplate parsed = new ArgumentParser(MyTemplate.class) {{
    +	 *     addCommand(new Command(MyTemplate.SubTemplate.class));
    +	 * }}
    +	 *     .parse(input)
    +	 *     .printErrors()
    +	 *     .exitIfErrors()
    +	 *     .into(MyTemplate.class);
    +	 * }
    + *

    + * Is equivalent to this code: + *

    {@code
    +	 * MyTemplate parsed = ArgumentParser.parseFromInto(MyTemplate.class, input);
    +	 * }
    +	 * 
    * - * @param args The command line arguments to parse. + * @param templateClass The class to use as a template. + * @param input The input to parse. + * @param options A consumer that can be used for configuring the parsing process. + * @param The type of the template. + * @return The parsed template. + * @see #parseFromInto(Class, CLInput) + * @see CommandTemplate */ - public @NotNull ParsedArgumentsRoot parseArgs(@NotNull String args) { - final var res = this.parseArgsNoExit(args); - final var errorCode = this.getErrorCode(); - - for (var msg : res.second()) { - System.err.println(msg); - } + public static @NotNull T parseFromInto( + @NotNull Class templateClass, + @NotNull CLInput input, + @NotNull Consumer<@NotNull AfterParseOptions> options + ) + { + final AfterParseOptions opts = ArgumentParser.from(templateClass).parse(input); + options.accept(opts); - if (errorCode != 0) { - System.exit(errorCode); - } + return opts.into(templateClass); + } - return res.first(); + /** + * Constructs a new {@link ArgumentParser} based on the given {@link CommandTemplate}, parses the given input, and + * populates the template with the parsed values. + * + * @param templateClass The class to use as a template. + * @param input The input to parse. + * @param The type of the template. + * @return The parsed template. + * @see CommandTemplate + */ + public static + @NotNull T parseFromInto(@NotNull Class templateClass, @NotNull CLInput input) { + return ArgumentParser.parseFromInto(templateClass, input, opts -> opts.printErrors().exitIfErrors()); } /** - * Parses the arguments from the sun.java.command system property. + * Adds all commands defined with {@link Command.Define} in the given class to the given parent command. This method + * is recursive and will add all sub-commands of the given class. + * + * @param templateClass The class to search for commands in. + * @param parentCommand The command to add the found commands to. + * @param The type of the class to search for commands in. */ - public @NotNull ParsedArguments parseArgs() { - var args = System.getProperty("sun.java.command").split(" "); - return this.parseArgs(Arrays.copyOfRange(args, 1, args.length)); + @SuppressWarnings("unchecked") + private static + void from$setCommands(@NotNull Class templateClass, @NotNull Command parentCommand) { + final var commandDefs = Arrays.stream(templateClass.getDeclaredClasses()) + .filter(c -> c.isAnnotationPresent(Command.Define.class)) + .filter(c -> Modifier.isStatic(c.getModifiers())) + .filter(CommandTemplate.class::isAssignableFrom) + .map(c -> (Class)c) + .toList(); + + for (var commandDef : commandDefs) { + var command = new Command(commandDef); + parentCommand.addCommand(command); + ArgumentParser.from$setCommands(commandDef, command); + } } - protected @NotNull Pair<@NotNull ParsedArgumentsRoot, @NotNull List<@NotNull String>> - parseArgsNoExit(@NotNull String args) - { + /** + * Parses the given command line arguments and returns a {@link AfterParseOptions} object. + * + * @param input The command line arguments to parse. + * @see AfterParseOptions + */ + public @NotNull AfterParseOptions parse(@NotNull CLInput input) { if (this.isParsed) { // reset all parsing related things to the initial state this.resetState(); @@ -72,20 +174,20 @@ public ArgumentParser(@NotNull String programName) { // pass the properties of this Sub-Command to its children recursively (most of the time this is what the user will want) this.passPropertiesToChildren(); - this.tokenize(args); // first. This will tokenize all Sub-Commands recursively + this.tokenize(input.args); // first. This will tokenize all Sub-Commands recursively var errorHandler = new ErrorHandler(this); - this.parse(); // same thing, this parses all the stuff recursively + this.parseTokens(); // same thing, this parses all the stuff recursively this.invokeCallbacks(); this.isParsed = true; - return new Pair<>(this.getParsedArguments(), errorHandler.handleErrorsGetMessages()); + return new AfterParseOptions(errorHandler); } + @Override - @NotNull - ParsedArgumentsRoot getParsedArguments() { + @NotNull ParsedArgumentsRoot getParsedArguments() { return new ParsedArgumentsRoot( this, this.getParser().getParsedArgumentsHashMap(), @@ -94,6 +196,9 @@ ParsedArgumentsRoot getParsedArguments() { ); } + /** + * Returns the forward value token (if any) of the full token list. + */ private @Nullable String getForwardValue() { final var tokens = this.getFullTokenList(); final var lastToken = tokens.get(tokens.size() - 1); @@ -104,19 +209,223 @@ ParsedArgumentsRoot getParsedArguments() { return null; } + /** + * Sets the license of this program. By default, this is shown in the help message. + * @param license The license information to set. + */ + public void setLicense(@NotNull String license) { + this.license = license; + } + + /** + * Returns the license of this program. + * @see #setLicense(String) + */ public @Nullable String getLicense() { return this.license; } - public void setLicense(@NotNull String license) { - this.license = license; + /** + * Sets the version of this program. By default, this is shown in the help message. + * @param version The version information to set. + */ + public void setVersion(@NotNull String version) { + this.version = version; } + /** + * Returns the version of this program. + * @see #setVersion(String) + */ public @Nullable String getVersion() { return this.version; } - public void setVersion(@NotNull String version) { - this.version = version; + + /** + * Provides utilities for the parsed arguments after parsing is done. + */ + public class AfterParseOptions { + private final List<@NotNull String> errors; + private final int errorCode; + + private AfterParseOptions(ErrorHandler errorHandler) { + this.errorCode = ArgumentParser.this.getErrorCode(); + this.errors = errorHandler.handleErrors(); + } + + /** + * Returns a list of all the error messages that occurred during parsing. + */ + public @NotNull List<@NotNull String> getErrors() { + return this.errors; + } + + /** + * @see Command#getErrorCode() + */ + public int getErrorCode() { + return this.errorCode; + } + + /** + * Returns whether any errors occurred during parsing. + * @return {@code true} if any errors occurred, {@code false} otherwise. + */ + public boolean hasErrors() { + return this.errorCode != 0; + } + + /** + * Prints all errors that occurred during parsing to {@link System#err}. + */ + public AfterParseOptions printErrors() { + for (var error : this.errors) { + System.err.println(error); + } + return this; + } + + /** + * Exits the program with the error code returned by {@link #getErrorCode()} if any errors occurred during + * parsing. + */ + public AfterParseOptions exitIfErrors() { + if (this.hasErrors()) + System.exit(this.errorCode); + + return this; + } + + /** + * Returns a {@link ParsedArgumentsRoot} object that contains all the parsed arguments. + */ + public @NotNull ParsedArgumentsRoot getParsedArguments() { + return ArgumentParser.this.getParsedArguments(); + } + + /** + * Instantiates the given Command Template class and sets all the fields annotated with {@link Argument.Define} + * corresponding to their respective parsed arguments. + * This method will also instantiate all the sub-commands recursively if defined in the template class properly. + * @param clazz The Command Template class to instantiate. + * @return The instantiated Command Template class. + * @param The type of the Command Template class. + * @see CommandTemplate + */ + public T into(@NotNull Class clazz) { + return AfterParseOptions.into(clazz, this.getParsedArguments()); + } + + /** + * {@link #into(Class)} helper method. + * @param clazz The Command Template class to instantiate. + * @param parsedArgs The parsed arguments to set the fields of the Command Template class. + */ + private static T into( + @NotNull Class clazz, + @NotNull ParsedArguments parsedArgs + ) + { + final T instance = UtlReflection.instantiate(clazz); + + Stream.of(clazz.getFields()) + .filter(f -> f.isAnnotationPresent(Argument.Define.class)) + .forEach(f -> { + final var annotation = f.getAnnotation(Argument.Define.class); + + // get the name of the argument from the annotation or field name + final String argName = annotation.names().length == 0 ? f.getName() : annotation.names()[0]; + + final @NotNull Optional parsedValue = parsedArgs.get(argName); + + try { + // if the field has a value already set and the parsed value is empty, skip it (keep the old value) + if (parsedValue.isEmpty() && f.get(instance) != null) + return; + + // if the type of the field is an Optional, wrap the value in it. + // otherwise, just set the value + f.set( + instance, + f.getType().isAssignableFrom(Optional.class) + ? parsedValue + : parsedValue.orElse(null) + ); + } catch (IllegalArgumentException e) { + if (parsedValue.isEmpty()) + throw new IncompatibleCommandTemplateType( + "Field '" + f.getName() + "' of type '" + f.getType().getSimpleName() + "' does not" + + " accept null values, but the parsed argument '" + argName + "' is null" + ); + + throw new IncompatibleCommandTemplateType( + "Field '" + f.getName() + "' of type '" + f.getType().getSimpleName() + "' is not " + + "compatible with the type (" + parsedValue.getClass().getSimpleName() + ") of the " + + "parsed argument '" + argName + "'" + ); + + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + + // now handle the sub-command field accessors (if any) + final var declaredClasses = Stream.of(clazz.getDeclaredClasses()) + .filter(c -> c.isAnnotationPresent(Command.Define.class)) + .toList(); + + for (var cls : declaredClasses) { + final var field = Stream.of(clazz.getDeclaredFields()) + .filter(f -> f.isAnnotationPresent(CommandTemplate.CommandAccessor.class)) + .filter(f -> f.getType() == cls) + .findFirst() + .orElseThrow(() -> { + throw new CommandTemplateException( + "The class '" + cls.getSimpleName() + "' is annotated with @Command.Define but it's " + + "enclosing class does not have a field annotated with @CommandAccessor" + ); + }); + + AfterParseOptions.into$handleCommandAccessor(instance, field, parsedArgs); + } + + return instance; + } + + /** + * {@link #into(Class)} helper method. Handles the {@link CommandTemplate.CommandAccessor} annotation. + * @param parsedTemplateInstance The instance of the current Command Template class. + * @param commandAccesorField The field annotated with {@link CommandTemplate.CommandAccessor}. + * @param parsedArgs The parsed arguments to set the fields of the Command Template class. + */ + @SuppressWarnings("unchecked") + private static void into$handleCommandAccessor( + @NotNull T parsedTemplateInstance, + @NotNull Field commandAccesorField, + @NotNull ParsedArguments parsedArgs + ) + { + final Class fieldType = commandAccesorField.getType(); + + if (!CommandTemplate.class.isAssignableFrom(fieldType)) + throw new CommandTemplateException( + "The field '" + commandAccesorField.getName() + "' is annotated with @CommandAccessor " + + "but its type is not a subclass of CommandTemplate" + ); + + final String cmdName = CommandTemplate.getTemplateNames((Class)fieldType)[0]; + + try { + commandAccesorField.set(parsedTemplateInstance, + AfterParseOptions.into( + (Class)fieldType, + parsedArgs.getSubParsedArgs(cmdName) + ) + ); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } } } \ No newline at end of file diff --git a/src/main/java/lanat/ArgumentType.java b/src/main/java/lanat/ArgumentType.java index 22873f9b..3f47156c 100644 --- a/src/main/java/lanat/ArgumentType.java +++ b/src/main/java/lanat/ArgumentType.java @@ -5,13 +5,40 @@ import lanat.utils.ErrorsContainerImpl; import lanat.utils.Range; import lanat.utils.Resettable; -import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.function.Consumer; +/** + *

    Argument Type

    + *

    + * An Argument Type is a handler in charge of parsing a specific kind of input from the command line. For example, the + * {@link IntegerArgumentType} is in charge of parsing integers from the command line. + *

    + *

    Creating custom Argument Types

    + *

    + * Creating new Argument Types is an easy task. Extending this class already provides you with most of the functionality + * that you need. The minimum method that should be implemented is the {@link ArgumentType#parseValues(String[])} method. + * Which will be called by the main parser when it needs to parse the values of an argument of this type. + *

    + * The custom Argument Type can push errors to the main parser by using the {@link ArgumentType#addError(String)} method + * and its overloads. + *

    + * It is possible to use other Argument Types inside your custom Argument Type. This is done by using the + * {@link ArgumentType#registerSubType(ArgumentType)} method. This allows you to listen for errors that occur in the + * subtypes, and to add them to the list of errors of the main parser. {@link ArgumentType#onSubTypeError(CustomError)} + * is called when an error occurs in a subtype. + *

    + *

    + * You can also implement {@link Parseable} to create a basic argument type implementation. Note that in order to + * use that implementation, you need to wrap it in a {@link FromParseableArgumentType} instance (which provides the + * necessary internal functionality). + *

    + * @param The type of the value that this argument type parses. + */ public abstract class ArgumentType extends ErrorsContainerImpl implements Resettable, Parseable, ParentElementGetter> @@ -39,7 +66,7 @@ public abstract class ArgumentType /** * This specifies the number of values that this argument received when being parsed. */ - private int lastReceivedValueCount = 0; + private int lastReceivedValuesNum = 0; /** This specifies the number of times this argument type has been used during parsing. */ short usageCount = 0; @@ -51,35 +78,39 @@ public abstract class ArgumentType private @Nullable ArgumentType parentArgType; private final @NotNull ArrayList<@NotNull ArgumentType> subTypes = new ArrayList<>(); + /** Mapping of types to their corresponding argument types. Used for inferring. */ + private static final HashMap, Class>> INFER_ARGUMENT_TYPES_MAP = new HashMap<>(); + + + /** + * Constructs a new argument type with the specified initial value. + * @param initialValue The initial value of this argument type. + */ public ArgumentType(@NotNull T initialValue) { this(); this.setValue(this.initialValue = initialValue); } + /** + * Constructs a new argument type with no initial value. + */ public ArgumentType() { if (this.getRequiredUsageCount().min() == 0) { throw new IllegalArgumentException("The required usage count must be at least 1."); } } - public final void parseAndUpdateValue(@NotNull String @NotNull [] args) { - this.lastReceivedValueCount = args.length; - this.currentValue = this.parseValues(args); - } - - public final void parseAndUpdateValue(@NotNull String arg) { - this.lastReceivedValueCount = 1; - this.currentValue = this.parseValues(arg); - } - - public final @Nullable T parseValues(@NotNull String arg) { - return this.parseValues(new String[] { arg }); + /** + * Saves the specified tokenIndex and the number of values received, and then parses the values. + * @param tokenIndex The index of the token that caused the parsing of this argument type. + * @param values The values to parse. + */ + public final void parseAndUpdateValue(short tokenIndex, @NotNull String... values) { + this.lastTokenIndex = tokenIndex; + this.lastReceivedValuesNum = values.length; + this.currentValue = this.parseValues(values); } - @Override - public abstract @Nullable T parseValues(@NotNull String @NotNull [] args); - - /** * By registering a subtype, this allows you to listen for errors that occurred in this subtype during parsing. The * {@link ArgumentType#onSubTypeError(CustomError)} method will be called when an error occurs. @@ -95,7 +126,7 @@ protected final void registerSubType(@NotNull ArgumentType subType) { /** * This is called when a subtype of this argument type has an error. By default, this adds the error to the list of - * errors, while also adding the {@link ArgumentType#currentArgValueIndex}. + * errors, while also adding the {@link ArgumentType#currentArgValueIndex} to the error's token index. * * @param error The error that occurred in the subtype. */ @@ -104,31 +135,48 @@ protected void onSubTypeError(@NotNull CustomError error) { this.addError(error); } + /** + * Dispatches the error to the parent argument type. + * @param error The error to dispatch. + */ private void dispatchErrorToParent(@NotNull CustomError error) { if (this.parentArgType != null) { this.parentArgType.onSubTypeError(error); } } + /** + * Returns the current value of this argument type. + * @return The current value of this argument type. + */ public T getValue() { return this.currentValue; } + /** + * Returns the final value of this argument type. This is the value that this argument type will have after parsing + * is done. + * @return The final value of this argument type. + */ + public T getFinalValue() { + return this.getValue(); // by default, the final value is just the current value. subclasses can override this. + } + /** * Sets the current value of this argument type. */ - public void setValue(@NotNull T value) { + protected void setValue(@NotNull T value) { this.currentValue = value; } + /** + * Returns the initial value of this argument type, if specified. + * @return The initial value of this argument type, {@code null} if not specified. + */ public T getInitialValue() { return this.initialValue; } - - /** - * Specifies the number of values that this argument should receive when being parsed. - */ @Override public @NotNull Range getRequiredArgValueCount() { return Range.ONE; @@ -144,22 +192,8 @@ public T getInitialValue() { return Range.ONE; } - /** Specifies the representation of this argument type. This may appear in places like the help message. */ - @Override - public @Nullable TextFormatter getRepresentation() { - return new TextFormatter(this.getClass().getSimpleName()); - } - - /** - * Returns the final value of this argument type. This is the value that this argument type has after parsing. - */ - public @Nullable T getFinalValue() { - return this.currentValue; - } - /** - * Adds an error to the list of errors that occurred during parsing. - * + * Adds an error to the list of errors that occurred during parsing at the current token index. * @param message The message to display related to the error. */ protected void addError(@NotNull String message) { @@ -168,7 +202,6 @@ protected void addError(@NotNull String message) { /** * Adds an error to the list of errors that occurred during parsing. - * * @param message The message to display related to the error. * @param index The index of the value that caused the error. */ @@ -178,7 +211,6 @@ protected void addError(@NotNull String message, int index) { /** * Adds an error to the list of errors that occurred during parsing. - * * @param message The message to display related to the error. * @param level The level of the error. */ @@ -188,52 +220,51 @@ protected void addError(@NotNull String message, @NotNull ErrorLevel level) { /** * Adds an error to the list of errors that occurred during parsing. - * * @param message The message to display related to the error. * @param index The index of the value that caused the error. * @param level The level of the error. */ protected void addError(@NotNull String message, int index, @NotNull ErrorLevel level) { - if (!this.getRequiredArgValueCount().isIndexInRange(index)) { - throw new IndexOutOfBoundsException("Index " + index + " is out of range for " + this.getClass().getName()); - } + this.addError(new CustomError(message, index, level)); + } + /** + * {@inheritDoc} + *

    + * Note: The error is modified to have the correct token index before being added to the list of + * errors. + *

    + */ + @Override + public void addError(@NotNull CustomError error) { if (this.lastTokenIndex == -1) { throw new IllegalStateException("Cannot add an error to an argument that has not been parsed yet."); } - var error = new CustomError( - message, - this.lastTokenIndex + Math.min(index + 1, this.lastReceivedValueCount), - level - ); - - super.addError(error); - this.dispatchErrorToParent(error); - } - - @Override - public void addError(@NotNull CustomError error) { - if (!this.getRequiredArgValueCount().isIndexInRange(error.tokenIndex)) { + // the index of the error should never be less than 0 or greater than the max value count + if (error.tokenIndex < 0 || error.tokenIndex >= this.getRequiredArgValueCount().max()) { throw new IndexOutOfBoundsException("Index " + error.tokenIndex + " is out of range for " + this.getClass().getName()); } - error.tokenIndex = this.lastTokenIndex + Math.min(error.tokenIndex + 1, this.lastReceivedValueCount); + // the index of the error should be relative to the last token index + error.tokenIndex = this.lastTokenIndex + Math.min(error.tokenIndex + 1, this.lastReceivedValuesNum); super.addError(error); this.dispatchErrorToParent(error); } + /** + * Returns the index of the last token that was parsed. + */ protected short getLastTokenIndex() { return this.lastTokenIndex; } - void setLastTokenIndex(short lastTokenIndex) { - this.lastTokenIndex = lastTokenIndex; - } - - int getLastReceivedValueCount() { - return this.lastReceivedValueCount; + /** + * Returns the number of values that this argument received when being parsed the last time. + */ + int getLastReceivedValuesNum() { + return this.lastReceivedValuesNum; } /** @@ -255,9 +286,12 @@ public void resetState() { this.currentValue = this.initialValue; this.lastTokenIndex = -1; this.currentArgValueIndex = 0; - this.lastReceivedValueCount = 0; + this.lastReceivedValuesNum = 0; this.usageCount = 0; - this.subTypes.forEach(ArgumentType::resetState); + this.subTypes.forEach(at -> { + at.resetState(); // reset the state of the subtypes. + at.lastTokenIndex = 0; // remember to reset this back to 0. otherwise, the subtype will throw an error! + }); } @Override @@ -265,56 +299,38 @@ public void resetState() { return this.parentArgType; } - // Easy to access values. These are methods because we don't want to use the same instance everywhere. - public static IntArgument INTEGER() { - return new IntArgument(); - } - - public static IntRangeArgument INTEGER_RANGE(int min, int max) { - return new IntRangeArgument(min, max); - } - - public static FloatArgument FLOAT() { - return new FloatArgument(); - } - - public static BooleanArgument BOOLEAN() { - return new BooleanArgument(); - } - - public static CounterArgument COUNTER() { - return new CounterArgument(); - } - - public static FileArgument FILE() { - return new FileArgument(); - } - - public static StringArgument STRING() { - return new StringArgument(); - } - - public static MultipleStringsArgument STRINGS() { - return new MultipleStringsArgument(); - } - public static , Ti> KeyValuesArgument KEY_VALUES(T valueType) { - return new KeyValuesArgument<>(valueType); - } - - public static > EnumArgument ENUM(T enumDefault) { - return new EnumArgument<>(enumDefault); - } - - public static StdinArgument STDIN() { - return new StdinArgument(); - } - - public static , Ti> FromParseableArgument FROM_PARSEABLE(T parseable) { - return new FromParseableArgument<>(parseable); + /** + * Registers an argument type to be inferred for the specified type/s. + * @param type The argument type to infer. + * @param infer The types to infer the argument type for. + */ + public static void registerTypeInfer(@NotNull Class> type, @NotNull Class... infer) { + for (Class clazz : infer) { + ArgumentType.INFER_ARGUMENT_TYPES_MAP.put(clazz, type); + } } - public static TryParseArgument TRY_PARSE(Class type) { - return new TryParseArgument<>(type); + /** + * Returns the argument type that should be inferred for the specified type. + * @param clazz The type to infer the argument type for. + * @return The argument type that should be inferred for the specified type. Returns {@code null} if no + * valid argument type was found. + */ + public static Class> getTypeInfer(@NotNull Class clazz) { + return ArgumentType.INFER_ARGUMENT_TYPES_MAP.get(clazz); + } + + // add some default argument types. + static { + // we need to also specify the primitives... wish there was a better way to do this. + ArgumentType.registerTypeInfer(StringArgumentType.class, String.class); + ArgumentType.registerTypeInfer(IntegerArgumentType.class, int.class, Integer.class); + ArgumentType.registerTypeInfer(BooleanArgumentType.class, boolean.class, Boolean.class); + ArgumentType.registerTypeInfer(FloatArgumentType.class, float.class, Float.class); + ArgumentType.registerTypeInfer(DoubleArgumentType.class, double.class, Double.class); + ArgumentType.registerTypeInfer(LongArgumentType.class, long.class, Long.class); + ArgumentType.registerTypeInfer(ShortArgumentType.class, short.class, Short.class); + ArgumentType.registerTypeInfer(ByteArgumentType.class, byte.class, Byte.class); } } \ No newline at end of file diff --git a/src/main/java/lanat/CLInput.java b/src/main/java/lanat/CLInput.java new file mode 100644 index 00000000..7c7f5d30 --- /dev/null +++ b/src/main/java/lanat/CLInput.java @@ -0,0 +1,46 @@ +package lanat; + +import org.jetbrains.annotations.NotNull; + +/** + * A class to gather the input from the command line. + */ +public final class CLInput { + /** + * The string of arguments passed to the program. + */ + public final @NotNull String args; + + private CLInput(@NotNull String args) { + this.args = args; + } + + /** + * Constructs a new {@link CLInput} from the given arguments array. + * @param args The array of arguments. + * @return A new {@link CLInput} from the given arguments array. + */ + public static @NotNull CLInput from(@NotNull String @NotNull [] args) { + return new CLInput(String.join(" ", args)); + } + + /** + * Constructs a new {@link CLInput} from the given arguments string. + * @param args The arguments string. + * @return A new {@link CLInput} from the given arguments string. + */ + public static @NotNull CLInput from(@NotNull String args) { + return new CLInput(args); + } + + /** + * Gets the arguments passed to the program from the system property {@code "sun.java.command"}. + * @return A new {@link CLInput} from the system property {@code "sun.java.command"}. + */ + public static @NotNull CLInput fromSystemProperty() { + final var args = System.getProperty("sun.java.command"); + + // remove first word from args (the program name) + return new CLInput(args.substring(args.indexOf(' ') + 1)); + } +} \ No newline at end of file diff --git a/src/main/java/lanat/CallbacksInvocationOption.java b/src/main/java/lanat/CallbacksInvocationOption.java index 38412d50..1dcf723f 100644 --- a/src/main/java/lanat/CallbacksInvocationOption.java +++ b/src/main/java/lanat/CallbacksInvocationOption.java @@ -1,5 +1,8 @@ package lanat; +/** + * @see Command#setCallbackInvocationOption(CallbacksInvocationOption) + */ public enum CallbacksInvocationOption { /** The callback will only be invoked when there are no errors in the argument. */ NO_ERROR_IN_ARGUMENT, diff --git a/src/main/java/lanat/Command.java b/src/main/java/lanat/Command.java index 2283542a..ff63c22f 100644 --- a/src/main/java/lanat/Command.java +++ b/src/main/java/lanat/Command.java @@ -1,9 +1,7 @@ package lanat; -import lanat.commandTemplates.DefaultCommandTemplate; -import lanat.exceptions.ArgumentAlreadyExistsException; -import lanat.exceptions.ArgumentGroupAlreadyExistsException; import lanat.exceptions.CommandAlreadyExistsException; +import lanat.exceptions.CommandTemplateException; import lanat.helpRepresentation.HelpFormatter; import lanat.parsing.Parser; import lanat.parsing.Token; @@ -15,11 +13,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.util.*; import java.util.function.Consumer; +import java.util.stream.Stream; /** *

    Command

    @@ -32,89 +33,121 @@ public class Command extends ErrorsContainerImpl implements ErrorCallbacks, - ArgumentAdder, - ArgumentGroupAdder, - Resettable, - MultipleNamesAndDescription, - ParentElementGetter, - CommandUser + ArgumentAdder, + ArgumentGroupAdder, + CommandAdder, + CommandUser, + Resettable, + MultipleNamesAndDescription, + ParentElementGetter { private final @NotNull List<@NotNull String> names = new ArrayList<>(); - public @Nullable String description; + private @Nullable String description; final @NotNull ArrayList<@NotNull Argument> arguments = new ArrayList<>(); final @NotNull ArrayList<@NotNull Command> subCommands = new ArrayList<>(); private Command parentCommand; final @NotNull ArrayList<@NotNull ArgumentGroup> argumentGroups = new ArrayList<>(); - private final @NotNull ModifyRecord<@NotNull TupleCharacter> tupleChars = new ModifyRecord<>(TupleCharacter.SQUARE_BRACKETS); - private final @NotNull ModifyRecord<@NotNull Integer> errorCode = new ModifyRecord<>(1); + private final @NotNull ModifyRecord<@NotNull TupleCharacter> tupleChars = ModifyRecord.of(TupleCharacter.SQUARE_BRACKETS); + private final @NotNull ModifyRecord<@NotNull Integer> errorCode = ModifyRecord.of(1); // error handling callbacks private @Nullable Consumer onErrorCallback; private @Nullable Consumer onCorrectCallback; - private final @NotNull ModifyRecord helpFormatter = new ModifyRecord<>(new HelpFormatter(this)); + private final @NotNull ModifyRecord helpFormatter = ModifyRecord.of(new HelpFormatter()); private final @NotNull ModifyRecord<@NotNull CallbacksInvocationOption> callbackInvocationOption = - new ModifyRecord<>(CallbacksInvocationOption.NO_ERROR_IN_ALL_COMMANDS); + ModifyRecord.of(CallbacksInvocationOption.NO_ERROR_IN_ALL_COMMANDS); /** A pool of the colors that an argument may have when being represented on the help. */ - final @NotNull LoopPool<@NotNull Color> colorsPool = LoopPool.atRandomIndex(Color.getBrightColors()); + final @NotNull LoopPool<@NotNull Color> colorsPool = LoopPool.atRandomIndex(Color.BRIGHT_COLORS.toArray(Color[]::new)); - public Command(@NotNull String name, @Nullable String description, CommandTemplate template) { + /** + * Creates a new command with the given name and description. + * @param name The name of the command. This is the name the user will use to indicate that they want to use this + * command. Must be unique among all the commands in the same parent command. + * @param description The description of the command. + * @see #setDescription(String) + */ + public Command(@NotNull String name, @Nullable String description) { this.addNames(name); this.description = description; - template.applyTo(this); - } - - public Command(@NotNull String name, @Nullable String description) { - this(name, description, new DefaultCommandTemplate()); } + /** + * Creates a new command with the given name and no description. This is the name the user will use to + * indicate that they want to use this command. + * @param name The name of the command. Must be unique among all the commands in the same parent command. + */ public Command(@NotNull String name) { this(name, null); } + /** + * Creates a new command based on the given {@link CommandTemplate}. This does not take Sub-Commands into account. + * @param templateClass The class of the template to use. + * @see CommandTemplate + */ + public Command(@NotNull Class templateClass) { + this.addNames(CommandTemplate.getTemplateNames(templateClass)); + this.from$recursive(templateClass); + } + @Override public , TInner> void addArgument(@NotNull Argument argument) { - argument.setParentCommand(this); // has to be done before checking for duplicates - if (this.arguments.stream().anyMatch(a -> a.equals(argument))) { - throw new ArgumentAlreadyExistsException(argument, this); - } + argument.registerToCommand(this); this.arguments.add(argument); + this.checkUniqueArguments(); } @Override public void addGroup(@NotNull ArgumentGroup group) { - if (this.argumentGroups.stream().anyMatch(g -> g.name.equals(group.name))) { - throw new ArgumentGroupAlreadyExistsException(group, this); - } - group.registerGroup(this); + group.registerToCommand(this); this.argumentGroups.add(group); + this.checkUniqueGroups(); } @Override - public @NotNull List<@NotNull ArgumentGroup> getSubGroups() { + public @NotNull List<@NotNull ArgumentGroup> getGroups() { return Collections.unmodifiableList(this.argumentGroups); } - public void addSubCommand(@NotNull Command cmd) { - if (this.subCommands.stream().anyMatch(a -> a.hasName(cmd.names.get(0)))) { - throw new CommandAlreadyExistsException(cmd, this); - } - + @Override + public void addCommand(@NotNull Command cmd) { if (cmd instanceof ArgumentParser) { throw new IllegalArgumentException("cannot add root command as Sub-Command"); } + if (cmd == this) { + throw new IllegalArgumentException("cannot add command to itself"); + } + + cmd.registerToCommand(this); this.subCommands.add(cmd); - cmd.parentCommand = this; + this.checkUniqueSubCommands(); } - public @NotNull List<@NotNull Command> getSubCommands() { + @Override + public void registerToCommand(@NotNull Command parentCommand) { + if (this.parentCommand != null) { + throw new CommandAlreadyExistsException(this, this.parentCommand); + } + + this.parentCommand = parentCommand; + } + + /** + * Returns a list of all the Sub-Commands that belong to this command. + * + * @return a list of all the Sub-Commands in this command + */ + @Override + public @NotNull List<@NotNull Command> getCommands() { return Collections.unmodifiableList(this.subCommands); } + /** * Specifies the error code that the program should return when this command failed to parse. When multiple commands * fail, the program will return the result of the OR bit operation that will be applied to all other command @@ -124,6 +157,7 @@ public void addSubCommand(@NotNull Command cmd) { *
  • Command 'bar' has a return value of 5. (0b101)
  • * * Both commands failed, so in this case the resultant return value would be 7 (0b111). + * @param errorCode The error code to return when this command fails. */ public void setErrorCode(int errorCode) { if (errorCode <= 0) throw new IllegalArgumentException("error code cannot be 0 or below"); @@ -139,18 +173,19 @@ public void setTupleChars(@NotNull TupleCharacter tupleChars) { } @Override - public Command addNames(String... names) { + public void addNames(@NotNull String... names) { Arrays.stream(names) - .forEach(n -> { - if (!UtlString.matchCharacters(n, Character::isAlphabetic)) - throw new IllegalArgumentException("Name " + UtlString.surround(n) + " contains non-alphabetic characters."); - - if (this.hasName(n)) - throw new IllegalArgumentException("Name " + UtlString.surround(n) + " is already used by this command."); + .map(UtlString::requireValidName) + .peek(newName -> { + if (this.hasName(newName)) + throw new IllegalArgumentException("Name " + UtlString.surround(newName) + " is already used by this command."); + }) + .forEach(this.names::add); - this.names.add(n); - }); - return this; + // now let the parent command know that this command has been modified. This is necessary to check + // for duplicate names + if (this.parentCommand != null) + this.parentCommand.checkUniqueSubCommands(); } @Override @@ -158,6 +193,7 @@ public Command addNames(String... names) { return this.names; } + @Override public void setDescription(@NotNull String description) { this.description = description; } @@ -168,7 +204,6 @@ public void setDescription(@NotNull String description) { } public void setHelpFormatter(@NotNull HelpFormatter helpFormatter) { - helpFormatter.setParentCmd(this); this.helpFormatter.set(helpFormatter); } @@ -177,12 +212,13 @@ public void setHelpFormatter(@NotNull HelpFormatter helpFormatter) { } /** - * Specifies in which cases the {@link Argument#onOk(Consumer)} should be invoked. + * Specifies in which cases the {@link Argument#setOnOkCallback(Consumer)} should be invoked. *

    By default, this is set to {@link CallbacksInvocationOption#NO_ERROR_IN_ALL_COMMANDS}.

    * + * @param option The option to set. * @see CallbacksInvocationOption */ - public void invokeCallbacksWhen(@NotNull CallbacksInvocationOption option) { + public void setCallbackInvocationOption(@NotNull CallbacksInvocationOption option) { this.callbackInvocationOption.set(option); } @@ -195,7 +231,7 @@ public void addError(@NotNull String message, @NotNull ErrorLevel level) { } public @NotNull String getHelp() { - return this.helpFormatter.get().toString(); + return this.helpFormatter.get().generate(this); } @Override @@ -208,7 +244,8 @@ public void addError(@NotNull String message, @NotNull ErrorLevel level) { } /** - * Returns true if an argument with {@link Argument#allowUnique()} in the command was used. + * Returns {@code true} if an argument with allowsUnique set in the command was used. + * @return {@code true} if an argument with {@link Argument#setAllowUnique(boolean)} in the command was used. */ public boolean uniqueArgumentReceivedValue() { return this.arguments.stream().anyMatch(a -> a.getUsageCount() >= 1 && a.isUniqueAllowed()) @@ -224,6 +261,10 @@ public boolean uniqueArgumentReceivedValue() { ); } + /** + * Returns a new {@link ParsedArguments} object that contains all the parsed arguments of this command and all its + * Sub-Commands. + */ @NotNull ParsedArguments getParsedArguments() { return new ParsedArguments( this, @@ -235,6 +276,7 @@ public boolean uniqueArgumentReceivedValue() { /** * Get all the tokens of all Sub-Commands (the ones that we can get without errors) into one single list. This * includes the {@link TokenType#COMMAND} tokens. + * @return A list of all the tokens of all Sub-Commands. */ public @NotNull ArrayList<@NotNull Token> getFullTokenList() { final ArrayList list = new ArrayList<>() {{ @@ -259,29 +301,107 @@ private void inheritProperties(@NotNull Command parent) { this.getMinimumExitErrorLevel().setIfNotModified(parent.getMinimumExitErrorLevel()); this.getMinimumDisplayErrorLevel().setIfNotModified(parent.getMinimumDisplayErrorLevel()); this.errorCode.setIfNotModified(parent.errorCode); - this.helpFormatter.setIfNotModified(() -> { - /* NEED TO BE COPIED!! If we don't then all commands will have the same formatter, - * which causes lots of problems. - * - * Stuff like the layout generators closures are capturing the reference to the previous Command - * and will not be updated properly when the parent command is updated. */ - var fmt = new HelpFormatter(parent.helpFormatter.get()); - fmt.setParentCmd(this); // we need to update the parent command! - return fmt; - }); + this.helpFormatter.setIfNotModified(parent.helpFormatter); this.callbackInvocationOption.setIfNotModified(parent.callbackInvocationOption); this.passPropertiesToChildren(); } + private void from$recursive(@NotNull Class cmdTemplate) { + if (!CommandTemplate.class.isAssignableFrom(cmdTemplate)) return; + + // don't allow classes without the @Command.Define annotation + if (!cmdTemplate.isAnnotationPresent(Command.Define.class)) { + throw new CommandTemplateException("The class '" + cmdTemplate.getName() + + "' is not annotated with @Command.Define"); + } + + // get to the top of the hierarchy + Optional.ofNullable(cmdTemplate.getSuperclass()).ifPresent(this::from$recursive); + + + var argumentBuildersFieldPairs = Stream.of(cmdTemplate.getDeclaredFields()) + .filter(f -> f.isAnnotationPresent(Argument.Define.class)) + .map(f -> new Pair<>(f, ArgumentBuilder.fromField(f))) + .toList(); + + var argumentBuilders = argumentBuildersFieldPairs.stream().map(Pair::second).toList(); + + this.from$invokeBeforeInitMethod(cmdTemplate, argumentBuilders); + + // set the argument types from the fields (if they are not already set) + argumentBuildersFieldPairs.forEach(pair -> pair.second().setArgTypeFromField(pair.first())); + + // add the arguments to the command + argumentBuilders.forEach(this::addArgument); + + this.from$invokeAfterInitMethod(cmdTemplate); + } + + private void from$invokeBeforeInitMethod( + @NotNull Class cmdTemplate, + @NotNull List> argumentBuilders + ) { + Stream.of(cmdTemplate.getDeclaredMethods()) + .filter(m -> UtlReflection.hasParameters(m, CommandTemplate.CommandBuildHelper.class)) + .filter(m -> m.isAnnotationPresent(CommandTemplate.InitDef.class)) + .filter(m -> m.getName().equals("beforeInit")) + .findFirst() + .ifPresent(m -> { + try { + m.invoke(null, new CommandTemplate.CommandBuildHelper( + this, Collections.unmodifiableList(argumentBuilders) + )); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + }); + } + + private void from$invokeAfterInitMethod(@NotNull Class cmdTemplate) { + Stream.of(cmdTemplate.getDeclaredMethods()) + .filter(m -> UtlReflection.hasParameters(m, Command.class)) + .filter(m -> m.isAnnotationPresent(CommandTemplate.InitDef.class)) + .filter(m -> m.getName().equals("afterInit")) + .findFirst() + .ifPresent(m -> { + try { + m.invoke(null, this); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + }); + } + void passPropertiesToChildren() { this.subCommands.forEach(c -> c.inheritProperties(this)); } + /** + * Returns {@code true} if the argument specified by the given name is equal to this argument. + *

    + * Equality is determined by the argument's name and the command it belongs to. + *

    + * + * @param obj the argument to compare to + * @return {@code true} if the argument specified by the given name is equal to this argument + */ + @Override + public boolean equals(@NotNull Object obj) { + if (obj instanceof Command cmd) + return UtlMisc.equalsByNamesAndParentCmd(this, cmd); + return false; + } + + void checkUniqueSubCommands() { + UtlMisc.requireUniqueElements(this.subCommands, c -> new CommandAlreadyExistsException(c, this)); + } + + // ------------------------------------------------ Error Handling ------------------------------------------------ @Override - public void setOnErrorCallback(@NotNull Consumer<@NotNull Command> callback) { + public void setOnErrorCallback(@Nullable Consumer<@NotNull Command> callback) { this.onErrorCallback = callback; } @@ -289,11 +409,11 @@ public void setOnErrorCallback(@NotNull Consumer<@NotNull Command> callback) { * {@inheritDoc} *

    * By default this callback is called only if all commands succeed, but you can change this behavior with - * {@link Command#invokeCallbacksWhen(CallbacksInvocationOption)} + * {@link Command#setCallbackInvocationOption(CallbacksInvocationOption)} *

    */ @Override - public void setOnCorrectCallback(@NotNull Consumer<@NotNull ParsedArguments> callback) { + public void setOnOkCallback(@Nullable Consumer<@NotNull ParsedArguments> callback) { this.onCorrectCallback = callback; } @@ -307,7 +427,11 @@ public void invokeCallbacks() { if (this.onErrorCallback != null) this.onErrorCallback.accept(this); } - this.parser.getParsedArgumentsHashMap().forEach(Argument::invokeCallbacks); + this.parser.getParsedArgumentsHashMap() + .entrySet() + .stream() + .sorted((x, y) -> Argument.compareByPriority(x.getKey(), y.getKey())) // sort by priority when invoking callbacks! + .forEach(e -> e.getKey().invokeCallbacks(e.getValue())); } boolean shouldExecuteCorrectCallback() { @@ -350,28 +474,28 @@ public boolean hasDisplayErrors() { } /** - * Get the error code of this Command. This is the OR of all the error codes of all the Sub-Commands that - * have failed. - * @see #setErrorCode(int) + * Get the error code of this Command. This is the OR of all the error codes of all the Sub-Commands that have + * failed. + * * @return The error code of this command. + * @see #setErrorCode(int) */ public int getErrorCode() { - int errCode = this.subCommands.stream() + final int thisErrorCode = this.errorCode.get(); + + // get all the error codes of the Sub-Commands recursively + int finalErrorCode = this.subCommands.stream() .filter(c -> c.tokenizer.isFinishedTokenizing()) - .map(sc -> - sc.getMinimumExitErrorLevel().get().isInErrorMinimum(this.getMinimumExitErrorLevel().get()) - ? sc.getErrorCode() - : 0 - ) + .map(Command::getErrorCode) .reduce(0, (a, b) -> a | b); /* If we have errors, or the Sub-Commands had errors, do OR with our own error level. * By doing this, the error code of a Sub-Command will be OR'd with the error codes of all its parents. */ - if (this.hasExitErrors() || errCode != 0) { - errCode |= this.errorCode.get(); + if (thisErrorCode != 0 && this.hasExitErrors()) { + finalErrorCode |= thisErrorCode; } - return errCode; + return finalErrorCode; } @@ -395,7 +519,7 @@ void tokenize(@NotNull String input) { this.tokenizer.tokenize(input); } - void parse() { + void parseTokens() { // first we need to set the tokens of all tokenized subCommands Command cmd = this; do { @@ -406,30 +530,6 @@ void parse() { this.parser.parseTokens(); } - /** - * Returns true if the argument specified by the given name is equal to this argument. - *

    - * Equality is determined by the argument's name and the command it belongs to. - *

    - * @param obj the argument to compare to - * @return true if the argument specified by the given name is equal to this argument - */ - public boolean equals(@NotNull Command obj) { - return Command.equalsByNamesAndParentCmd(this, obj); - } - - public static & CommandUser> - boolean equalsByNamesAndParentCmd(@NotNull T a, @NotNull T b) { - return a.getParentCommand() == b.getParentCommand() && ( - a.getNames().stream().anyMatch(name -> { - for (var otherName : b.getNames()) { - if (name.equals(otherName)) return true; - } - return false; - }) - ); - } - @Override public void resetState() { this.tokenizer = new Tokenizer(this); @@ -449,5 +549,12 @@ public void resetState() { public @Nullable Command getParentCommand() { return this.getParent(); } -} + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Define { + String[] names() default {}; + + String description() default ""; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/CommandAdder.java b/src/main/java/lanat/CommandAdder.java new file mode 100644 index 00000000..3cfd186f --- /dev/null +++ b/src/main/java/lanat/CommandAdder.java @@ -0,0 +1,45 @@ +package lanat; + +import lanat.exceptions.CommandNotFoundException; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * An interface for objects that can add {@link Command}s to themselves. + */ +public interface CommandAdder { + void addCommand(@NotNull Command command); + + /** + * Returns a list of all the Sub-Commands that belong to this command. + * + * @return a list of all the Sub-Commands in this command + */ + @NotNull List<@NotNull Command> getCommands(); + + default boolean hasCommand(@NotNull String name) { + for (final var command : this.getCommands()) { + if (command.getName().equals(name)) { + return true; + } + } + return false; + } + + /** + * Returns the Sub-Command with the specified name. + * + * @param name the name of the command to get + * @return the command with the specified name + * @throws CommandNotFoundException if no command with the specified name exists + */ + default @NotNull Command getCommand(@NotNull String name) { + for (final var command : this.getCommands()) { + if (command.getName().equals(name)) { + return command; + } + } + throw new CommandNotFoundException(name); + } +} diff --git a/src/main/java/lanat/CommandTemplate.java b/src/main/java/lanat/CommandTemplate.java index 324224b0..bd4a4e99 100644 --- a/src/main/java/lanat/CommandTemplate.java +++ b/src/main/java/lanat/CommandTemplate.java @@ -1,45 +1,224 @@ package lanat; -import fade.mirror.exception.MirrorException; -import fade.mirror.filter.Filter; +import lanat.exceptions.ArgumentNotFoundException; +import org.jetbrains.annotations.NotNull; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; -import static fade.mirror.Mirror.mirror; +/** + *

    Command Template

    + * Define the arguments and attributes of a command by defining the structure of a class. + *

    + *

    Creating a Template

    + *

    + * In order to define a new Command Template, create a new class that extends {@link CommandTemplate} and annotate it + * with {@link Command.Define}. The class must be public and static. + *

    + *

    + * A Command Template can inherit from another Command Template, which will also inherit the arguments and sub-commands + * of the parent. Command Templates are initialized from the parent to the child, so the parent's arguments will be + * initialized before the child's. + *

    + * + * Example: + *
    {@code
    + * @Command.Define(description = "My cool program")
    + * public class MyProgram extends CommandTemplate {
    + *   // ...
    + * }
    + * }
    + * + *

    Defining the Arguments

    + * To define an argument that will belong to the command, create a public field with the + * type that the parsed argument value should be. Annotate it with {@link Argument.Define}. + *

    + * The name of the argument may be specified in the annotation with the {@link Argument.Define#names()} parameter. + * If no name is specified, the name of the field will be used. + *

    + *

    + * The type of the argument (that extends {@link ArgumentType}) may be specified in the annotation with the + * {@link Argument.Define#argType()} parameter. Note that any type specified in the annotation must have a public, + * no-argument constructor. If the Argument Type to use has a constructor with arguments, the type must be then + * specified in {@link CommandTemplate#beforeInit(CommandBuildHelper)} instead, by setting + * {@link ArgumentBuilder#withArgType(ArgumentType)} to the argument builder corresponding to the argument + * being defined. + *

    + *

    + * If no Argument Type is specified on the annotation, the Argument Type will be attempted to be inferred from the + * field type if possible, which is the case for some built-in types, such as + * {@link String}, {@link Integer}, {@link Double}, etc. + *

    + * + * Example: + *
    {@code
    + * @Command.Define
    + * public class ParentCommand extends CommandTemplate {
    + *   @Argument.Define(names = {"name", "n"}, argType = StringArgumentType.class)
    + *   public String name;
    + *
    + *   @Argument.Define(argType = IntegerArgumentType.class, obligatory = true)
    + *   public Integer number;
    + * }
    + * }
    + * + *

    Defining Sub-Commands

    + *

    + * To define a sub-command, create a new inner class inside the parent Command Template class already defined + * (with same rules as for any Command Template class). + * Then, create a public field in the parent command template class with the type of the sub-command + * Command Template just created. Annotate it with {@link CommandAccessor}. This will properly link the sub-command + * to the parent command when the parent command is initialized. + *

    + * Example: + *
    {@code
    + * @Command.Define
    + * public class ParentCommand extends CommandTemplate {
    + *   @CommandAccessor
    + *   public SubCommand subCommand;
    + *
    + *   public static class SubCommand extends CommandTemplate {
    + *      // ...
    + *   }
    + * }
    + * }
    + * + *

    Other actions

    + *

    + * In order to configure the command more precisely, two public static methods with the {@link InitDef} annotation + * may be defined in the Command Template class: + *

    + *
      + *
    • {@link CommandTemplate#beforeInit(CommandBuildHelper)}: Called before adding the Arguments to the Command.
    • + *
    • {@link CommandTemplate#afterInit(Command)}: Called after the Command is initialized.
    • + *
    + * @see CommandTemplate.Default + */ +@Command.Define public abstract class CommandTemplate { - private Command command; + /** + * Annotation used to define an init method for a Command Template. + * @see CommandTemplate#beforeInit(CommandBuildHelper) + * @see CommandTemplate#afterInit(Command) + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + protected @interface InitDef {} - protected final Command cmd() { - return this.command; + /** + * Annotation used to define a sub-command field in a Command Template. + * @see CommandTemplate + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + protected @interface CommandAccessor {} + + /** + * Helper class that contains the command being initialized and the list of argument builders that may be altered. + * @param cmd The command being initialized. + * @param args The list of argument builders that may be altered. Use {@link CommandBuildHelper#arg(String)} + * to get the argument builder corresponding to an argument with a given name. + */ + public record CommandBuildHelper(@NotNull Command cmd, @NotNull List> args) { + @SuppressWarnings("unchecked") + public , TInner> + ArgumentBuilder arg(@NotNull String name) { + return (ArgumentBuilder)this.args.stream() + .filter(a -> a.hasName(name)) + .findFirst() + .orElseThrow(() -> new ArgumentNotFoundException(name)); + } } - @SuppressWarnings("unchecked") - final void applyTo(Command command) { - this.command = command; - - // get all the methods with the @ArgDef annotation, and add them to the command - mirror(this.getClass()) - .getMethods(Filter.forMethods().ofType(Argument.class).withAnnotation(ArgDef.class).withNoParameters()) - .forEach(method -> { - try { - var arg = method.bindToObject(this).invoke(); - if (arg == null) return; - this.command.addArgument(arg); - } catch (MirrorException exception) { - exception.printStackTrace(); - } - }); + // Dummy methods so that we prevent the user from creating an instance method with the same name. + + /** + * This method is called after the Command Template builder reads all the field arguments defined. + * This is before the Arguments are instantiated and finally added to the command. + *

    + * Example: + *

    {@code
    +	 * @Command.Define
    +	 * public class ParentCommand extends CommandTemplate {
    +	 *   @Argument.Define
    +	 *   public Integer numberRange;
    +	 *
    +	 *   @InitDef
    +	 *   public static void beforeInit(CommandBuildHelper cmdBuildHelper) {
    +	 *      // set the argument type to NumberRangeArgumentType
    +	 *      cmdBuildHelper., Integer>getArgument("numberRange")
    +	 * 			.withArgType(new NumberRangeArgumentType<>(0, 10);
    +	 *   }
    +	 * }
    +	 * }
    + + * @param cmdBuildHelper A helper object that contains the command being initialized and the list of argument builders that may + * be altered. + */ + @InitDef + public static void beforeInit(@NotNull CommandBuildHelper cmdBuildHelper) {} + + /** + * This method is called after the Command is initialized. This is after the Arguments are instantiated and added + * to the command. This method may be used to create groups of arguments, for example. + * @param cmd The command that was fully initialized. + */ + @InitDef + public static void afterInit(@NotNull Command cmd) {} + + /** + * Returns the names of the command template. If no names are specified in the annotation, the simple name of the + * class will be used. + * Note: Expects the field to be annotated with {@link Command.Define} + * + * @param cmdTemplate The command template class. Must be annotated with {@link Command.Define}. + * @return The names of the command template. + */ + public static @NotNull String @NotNull [] getTemplateNames(@NotNull Class cmdTemplate) { + final var annotation = cmdTemplate.getAnnotation(Command.Define.class); + assert annotation != null : "Command Template class must be annotated with @Command.Define"; + + final var annotationNames = annotation.names(); + + // if no names are specified, use the simple name of the class + return annotationNames.length == 0 ? + new String[] { cmdTemplate.getSimpleName() } + : annotationNames; } /** - * Annotation for methods that are used to define arguments to the command. - * The method must return an {@link Argument} object and take no parameters. + * A default command template that adds the 'help' and 'version' arguments to the command. + *
      + *
    • The 'help' argument shows the help message of the command (provided by the {@link Command#getHelp()} method).
    • + *
    • The 'version' argument shows the version of the program (provided by the {@link ArgumentParser#getVersion()} method).
    • + *
    */ - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - protected @interface ArgDef {} + @Command.Define + public static class Default extends CommandTemplate { + /* + * The reason we add these arguments here is so that they do not "physically" appear in the + * actual class that extends this one. 'help' and 'version' are just + * arguments that execute actions, and they not really provide any useful values. + */ + @InitDef + public static void afterInit(@NotNull Command cmd) { + cmd.addArgument(Argument.createOfBoolType("help") + .onOk(t -> System.out.println(cmd.getHelp())) + .withDescription("Shows this message.") + .allowsUnique() + ); + + if (cmd instanceof ArgumentParser ap) { + cmd.addArgument(Argument.createOfBoolType("version") + .onOk(t -> System.out.println("Version: " + ap.getVersion())) + .withDescription("Shows the version of this program.") + .allowsUnique() + ); + } + } + } } \ No newline at end of file diff --git a/src/main/java/lanat/CommandUser.java b/src/main/java/lanat/CommandUser.java index c07f62c0..3bfdb0fe 100644 --- a/src/main/java/lanat/CommandUser.java +++ b/src/main/java/lanat/CommandUser.java @@ -1,12 +1,21 @@ package lanat; +import org.jetbrains.annotations.NotNull; + /** - * This interface is used for getting the parent Command of an object that is part of a command. + * This interface is used for objects that belong to a {@link Command}. */ public interface CommandUser { /** * Gets the Command object that this object belongs to. + * * @return The parent command of this object. */ Command getParentCommand(); + + /** + * Sets the parent command of this object. + * @param parentCommand the parent command to set + */ + void registerToCommand(@NotNull Command parentCommand); } diff --git a/src/main/java/lanat/ErrorFormatter.java b/src/main/java/lanat/ErrorFormatter.java index 252f7e4d..9e591f1e 100644 --- a/src/main/java/lanat/ErrorFormatter.java +++ b/src/main/java/lanat/ErrorFormatter.java @@ -3,6 +3,7 @@ import lanat.helpRepresentation.HelpFormatter; import lanat.parsing.Token; import lanat.parsing.errors.ErrorHandler; +import lanat.utils.ErrorLevelProvider; import lanat.utils.UtlString; import lanat.utils.displayFormatter.FormatOption; import lanat.utils.displayFormatter.TextFormatter; @@ -11,34 +12,72 @@ import java.util.ArrayList; import java.util.List; +/** + * Class used by error handlers to easily format errors to be displayed to the user. + */ public class ErrorFormatter { - private @NotNull String contents = ""; + private @NotNull String content = ""; private DisplayTokensOptions tokensViewOptions; private @NotNull ErrorLevel errorLevel; + + /** Allows this class to provide some proxy instance methods to the {@link ErrorFormatter} instance. */ private final @NotNull ErrorHandler mainErrorHandler; + + /** The generator used to generate the error message. */ public static @NotNull ErrorFormatterGenerator generator = new ErrorFormatterGenerator(); + /** + * Creates a new error formatter + * @param mainErrorHandler The error handler that created this error formatter. + * @param level The error level of the error. + */ public ErrorFormatter(@NotNull ErrorHandler mainErrorHandler, @NotNull ErrorLevel level) { this.errorLevel = level; this.mainErrorHandler = mainErrorHandler; ErrorFormatter.generator.setErrorFormatter(this); } - public ErrorFormatter setContents(@NotNull String contents) { - this.contents = contents; + /** + * Sets the content of the error message. + * @param content The content of the error message. + * @return This instance. + */ + public ErrorFormatter setContent(@NotNull String content) { + this.content = content; return this; } + /** + * Sets the error level of the error to be displayed. This replaces the default error level provided in the + * constructor. + * @param errorLevel The error level of the error message. + * @return This instance. + */ public ErrorFormatter setErrorLevel(@NotNull ErrorLevel errorLevel) { this.errorLevel = errorLevel; return this; } + /** + * Returns the generated error message. + * @return The generated error message. + */ @Override public String toString() { return ErrorFormatter.generator.generate(); } + /** + * Indicates the generator to display all tokens. + *

    + * Tokens between the index {@code start} and the {@code offset} from it will be highlighted. If {@code placeArrow} + * is {@code true}, an arrow will be placed at each token index in that range. + *

    + * @param start The index of the first token to highlight. + * @param offset The number of tokens to highlight after the token at the index {@code start}. A value of {@code 0} + * may be used to highlight only the token at the index {@code start}. + * @param placeArrow Whether to place an arrow at each token index in the range. + */ public ErrorFormatter displayTokens(int start, int offset, boolean placeArrow) { this.tokensViewOptions = new DisplayTokensOptions( start + this.mainErrorHandler.getAbsoluteCmdTokenIndex(), offset, placeArrow @@ -46,41 +85,77 @@ public ErrorFormatter displayTokens(int start, int offset, boolean placeArrow) { return this; } + /** + * Indicates the generator to display all tokens. Places an error at the token at index {@code index}. + * @param index The index of the token to highlight. + */ public ErrorFormatter displayTokens(int index) { return this.displayTokens(index, 0, true); } - public record DisplayTokensOptions(int start, int offset, boolean placeArrow) {} + /** + * Options used to display tokens. + */ + public record DisplayTokensOptions(int start, int offset, boolean placeArrow) { + public DisplayTokensOptions { + if (start < 0) throw new IllegalArgumentException("start must be positive"); + if (offset < 0) throw new IllegalArgumentException("offset must be positive"); + } + } - public static class ErrorFormatterGenerator { + /** + * Class used to generate error messages from an {@link ErrorFormatter} instance. + *

    + * Methods {@link #generate()} and {@link #generateTokensView(DisplayTokensOptions)} provide the + * functionality to generate the error message. These may be overridden to provide custom error messages. + *

    + * Note: A single instance of this class is used to generate all messages for the errors. + */ + public static class ErrorFormatterGenerator implements ErrorLevelProvider { + /** The error formatter instance to generate the error message from. */ private ErrorFormatter errorFormatter; + /** + * Sets the error formatter instance to generate the error message from. + * @param errorFormatter The error formatter instance to generate the error message from. + */ private void setErrorFormatter(@NotNull ErrorFormatter errorFormatter) { this.errorFormatter = errorFormatter; } + /** + * Generates the error message to be displayed to the user. + * @return The error message to be displayed to the user. + */ public @NotNull String generate() { // first figure out the length of the longest line final var maxLength = UtlString.getLongestLine(this.getContents()).length(); final var formatter = this.getErrorLevelFormatter(); - final String tokensFormatting = this.getTokensViewFormatting(); + final String tokensFormatting = this.getTokensView(); - return formatter.setContents(" ┌─%s%s".formatted(this.getErrorLevel(), !tokensFormatting.isEmpty() ? "\n" : "")).toString() - + tokensFormatting + return formatter.withContents(" ┌─%s".formatted(this.getErrorLevel())).toString() + // only add a new line if there are tokens to display + + (tokensFormatting.isEmpty() ? "" : "\n" + tokensFormatting) // first insert a vertical bar at the start of each line - + this.getContentsWrapped().replaceAll("^|\\n", formatter.setContents("\n │ ").toString()) + + this.getContentsWrapped().replaceAll("^|\\n", formatter.withContents("\n │ ").toString()) // then insert a horizontal bar at the end, with the length of the longest line approximately - + formatter.setContents("\n └" + "─".repeat(Math.max(maxLength - 5, 0)) + " ───── ── ─") + + formatter.withContents("\n └" + "─".repeat(Math.max(maxLength - 5, 0)) + " ───── ── ─") + '\n'; } - protected @NotNull String generateTokensViewFormatting(DisplayTokensOptions options) { - final var arrow = TextFormatter.ERROR("<-").setColor(this.getErrorLevel().color); + /** + * Generates the tokens view to be displayed to the user. + * @param options The options to use to generate the tokens view. + * @return The tokens view to be displayed to the user. + */ + protected @NotNull String generateTokensView(DisplayTokensOptions options) { + final var arrow = TextFormatter.ERROR("<-").withForegroundColor(this.getErrorLevel().color); final var tokensFormatters = new ArrayList<>(this.getTokensFormatters()); final int tokensLength = tokensFormatters.size(); + // add an arrow at the start or end if the index is out of bounds if (options.start < 0) { tokensFormatters.add(0, arrow); } else if (options.start >= tokensLength) { @@ -88,16 +163,18 @@ private void setErrorFormatter(@NotNull ErrorFormatter errorFormatter) { } for (int i = 0; i < tokensLength; i++) { + // dim tokens before the command if (i < this.getAbsoluteCmdTokenIndex()) { tokensFormatters.get(i).addFormat(FormatOption.DIM); } + // highlight tokens in the range if (i >= options.start && i < options.start + options.offset + 1) { if (options.placeArrow) { tokensFormatters.add(i, arrow); } else { tokensFormatters.get(i) - .setColor(this.getErrorLevel().color) + .withForegroundColor(this.getErrorLevel().color) .addFormat(FormatOption.REVERSE, FormatOption.BOLD); } } @@ -106,48 +183,94 @@ private void setErrorFormatter(@NotNull ErrorFormatter errorFormatter) { return String.join(" ", tokensFormatters.stream().map(TextFormatter::toString).toList()); } - protected final @NotNull String getTokensViewFormatting() { + /** + * Returns the tokens view to be displayed to the user. This is the result of calling + * {@link #generateTokensView(DisplayTokensOptions)} with the options provided in the {@link ErrorFormatter} + * @return The tokens view to be displayed to the user. An empty string if no tokens view options were provided. + */ + protected final @NotNull String getTokensView() { final var options = this.errorFormatter.tokensViewOptions; if (options == null) return ""; - return this.generateTokensViewFormatting(options); + return this.generateTokensView(options); } + /** + * @see ErrorHandler#getAbsoluteCmdTokenIndex() + */ protected final int getAbsoluteCmdTokenIndex() { return this.errorFormatter.mainErrorHandler.getAbsoluteCmdTokenIndex(); } - protected final @NotNull ErrorLevel getErrorLevel() { + /** + * Returns the error level of the error currently being formatted. + * @return The error level of the error currently being formatted. + */ + @Override + public final @NotNull ErrorLevel getErrorLevel() { return this.errorFormatter.errorLevel; } + /** + * Returns a {@link TextFormatter} instance using the color and contents of the error + * level from {@link #getErrorLevel()}. + * @return A {@link TextFormatter} instance using the color and contents of the error level. + */ protected final @NotNull TextFormatter getErrorLevelFormatter() { - final var formatter = this.getErrorLevel(); - return new TextFormatter(formatter.toString(), formatter.color).addFormat(FormatOption.BOLD); + final var errorLevel = this.getErrorLevel(); + return new TextFormatter(errorLevel.toString(), errorLevel.color).addFormat(FormatOption.BOLD); } + /** + * Returns the whole list of tokens from the {@link ErrorHandler} instance. + * @return The whole list of tokens. + */ protected final @NotNull List<@NotNull Token> getTokens() { return this.errorFormatter.mainErrorHandler.tokens; } + /** + * Returns the whole list of tokens from the {@link ErrorHandler} instance, each mapped to a + * {@link TextFormatter} instance. + * @return The whole list of tokens mapped to a {@link TextFormatter} instance. + * @see Token#getFormatter() + * @see #getTokens() + */ protected final @NotNull List<@NotNull TextFormatter> getTokensFormatters() { return this.getTokens().stream().map(Token::getFormatter).toList(); } + /** + * Returns the text contents of the error message. + * @return The text contents of the error message. + */ protected final @NotNull String getContents() { - return this.errorFormatter.contents; + return this.errorFormatter.content; } + /** + * Returns the text contents of the error message, wrapped to the maximum line length in + * {@link HelpFormatter#lineWrapMax}. + * @return The text contents of the error message, wrapped to the maximum line length. + * @see UtlString#wrap(String, int) + */ protected final @NotNull String getContentsWrapped() { return UtlString.wrap(this.getContents(), HelpFormatter.lineWrapMax); } + /** + * Returns the text contents of the error message, with all new lines replaced with a single space. + * @return The text contents of the error message, with all new lines replaced with a single space. + */ protected final @NotNull String getContentsSingleLine() { return this.getContents().replaceAll("\n", " "); } + /** + * @see ErrorHandler#getRootCommand() + */ protected final @NotNull Command getRootCommand() { - return this.errorFormatter.mainErrorHandler.getRootCmd(); + return this.errorFormatter.mainErrorHandler.getRootCommand(); } } } diff --git a/src/main/java/lanat/MultipleNamesAndDescription.java b/src/main/java/lanat/MultipleNamesAndDescription.java index 3cdef282..e5a5fec4 100644 --- a/src/main/java/lanat/MultipleNamesAndDescription.java +++ b/src/main/java/lanat/MultipleNamesAndDescription.java @@ -3,43 +3,56 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; -public interface MultipleNamesAndDescription extends NamedWithDescription { +/** + * Represents an object that has multiple names and a description. + */ +public interface MultipleNamesAndDescription extends NamedWithDescription { /** * Add one or more names to this object. + * * @param names The names to add - * @return This object - * */ - T addNames(@NotNull String... names); + */ + void addNames(@NotNull String... names); /** * Returns all the names of this object. Will always return at least one. + * * @return All the names of this object. - * */ + */ @NotNull List<@NotNull String> getNames(); /** * {@inheritDoc} If multiple names are defined, the longest name will be returned. + * * @return The name of this object - * */ + */ @Override default @NotNull String getName() { final var names = this.getNames(); if (names.size() == 1) return names.get(0); - return new ArrayList<>(this.getNames()) {{ - this.sort((a, b) -> b.length() - a.length()); + return new ArrayList<>(names) {{ + this.sort(Comparator.comparingInt(String::length).reversed()); }}.get(0); } /** * Checks if this object has the given name. + * * @param name The name to check - * @return true if this object has the given name, false otherwise - * */ + * @return {@code true} if this object has the given name, {@code false} otherwise + */ default boolean hasName(String name) { return this.getNames().contains(name); } + + /** + * Sets the description of this object. The description is used to be displayed in the help message. + * @param description The description to set + */ + void setDescription(@NotNull String description); } diff --git a/src/main/java/lanat/NamedWithDescription.java b/src/main/java/lanat/NamedWithDescription.java index abe3246e..e1fdd8a0 100644 --- a/src/main/java/lanat/NamedWithDescription.java +++ b/src/main/java/lanat/NamedWithDescription.java @@ -3,15 +3,20 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * Represents an object that has a name and a description. + */ public interface NamedWithDescription { /** * Returns the name of this object. + * * @return The name of this object */ @NotNull String getName(); /** * Returns the description of this object. + * * @return The description of this object */ @Nullable String getDescription(); diff --git a/src/main/java/lanat/ParentElementGetter.java b/src/main/java/lanat/ParentElementGetter.java index 6f5bcaac..c7ed905b 100644 --- a/src/main/java/lanat/ParentElementGetter.java +++ b/src/main/java/lanat/ParentElementGetter.java @@ -4,27 +4,35 @@ import org.jetbrains.annotations.Nullable; /** - * This interface is used for getting the parent object of an object. - * This interface also provides a method with a default implementation for getting the root object. + * This interface is used for getting the parent object of an object. This interface also provides a method with a + * default implementation for getting the root object. + * * @param The type of the parent object. */ public interface ParentElementGetter> { /** * Gets the parent object of this object. + * * @return The parent object of this object. */ @Nullable T getParent(); /** - * Gets the root object in the hierarchy it belongs to. If this object is already the root, then this - * object is returned. + * Gets the root object in the hierarchy it belongs to. If this object is already the root, then this object is + * returned. + * * @return The root object of this element. */ @SuppressWarnings("unchecked") default @NotNull T getRoot() { T root = (T)this; - while (root.getParent() != null) - root = root.getParent(); + T parent = root.getParent(); + + while (parent != null) { + root = parent; + parent = root.getParent(); + } + return root; } } diff --git a/src/main/java/lanat/ParsedArguments.java b/src/main/java/lanat/ParsedArguments.java index 49fe6060..98689868 100644 --- a/src/main/java/lanat/ParsedArguments.java +++ b/src/main/java/lanat/ParsedArguments.java @@ -2,6 +2,7 @@ import lanat.exceptions.ArgumentNotFoundException; import lanat.exceptions.CommandNotFoundException; +import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -9,10 +10,6 @@ import java.util.HashMap; import java.util.List; import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.regex.Pattern; /** * Container for all the parsed arguments and their respective values. @@ -21,11 +18,10 @@ public class ParsedArguments { private final @NotNull HashMap<@NotNull Argument, @Nullable Object> parsedArgs; private final @NotNull Command cmd; private final @NotNull List<@NotNull ParsedArguments> subParsedArguments; - private static @NotNull String separator = "."; ParsedArguments( @NotNull Command cmd, - @NotNull HashMap, Object> parsedArgs, + @NotNull HashMap, @Nullable Object> parsedArgs, @NotNull List subParsedArguments ) { @@ -34,35 +30,24 @@ public class ParsedArguments { this.subParsedArguments = subParsedArguments; } - /** - * Specifies the separator to use when using the {@link #get(String)} method. By default, this is set to - * . - * @param separator The separator to use - */ - public static void setSeparator(@NotNull String separator) { - if (separator.isEmpty()) { - throw new IllegalArgumentException("separator cannot be empty"); - } - ParsedArguments.separator = separator; - } - /** * Returns the parsed value of the argument with the given name. + * * @param arg The argument to get the value of * @param The type of the value of the argument */ @SuppressWarnings("unchecked") // we'll just have to trust the user - public ParsedArgument get(@NotNull Argument arg) { + public @NotNull Optional get(@NotNull Argument arg) { if (!this.parsedArgs.containsKey(arg)) { throw new ArgumentNotFoundException(arg); } - return new ParsedArgument<>((T)this.parsedArgs.get(arg)); + return Optional.ofNullable((T)this.parsedArgs.get(arg)); } /** * Returns the parsed value of the argument with the given name. In order to access arguments in sub-commands, use - * the separator specified by {@link #setSeparator(String)}. (By default, this is .) + * the . separator to specify the route to the argument. * *

    * @@ -73,15 +58,15 @@ public ParsedArgument get(@NotNull Argument arg) { *

    * More info at {@link #get(String...)} * - * @param argRoute The route to the argument, separated by a separator set by {@link #setSeparator(String)} - * (default is .) + * @param argRoute The route to the argument, separated by the . character. * @param The type of the value of the argument. This is used to avoid casting. A type that does not match the - * argument's type will result in a {@link ClassCastException}. + * argument's type will result in a {@link ClassCastException}. + * @return The parsed value of the argument with the given name. * @throws CommandNotFoundException If the command specified in the route does not exist * @throws ArgumentNotFoundException If the argument specified in the route does not exist */ - public ParsedArgument get(@NotNull String argRoute) { - return this.get(argRoute.split(" *" + Pattern.quote(ParsedArguments.separator) + " *")); + public @NotNull Optional get(@NotNull String argRoute) { + return this.get(UtlString.split(argRoute, '.')); } @@ -106,10 +91,11 @@ public ParsedArgument get(@NotNull String argRoute) { * * * + * * @throws CommandNotFoundException If the command specified in the route does not exist */ @SuppressWarnings("unchecked") // we'll just have to trust the user - public ParsedArgument get(@NotNull String... argRoute) { + public @NotNull Optional get(@NotNull String... argRoute) { if (argRoute.length == 0) { throw new IllegalArgumentException("argument route must not be empty"); } @@ -117,7 +103,7 @@ public ParsedArgument get(@NotNull String... argRoute) { ParsedArguments matchedParsedArgs; if (argRoute.length == 1) { - return (ParsedArgument)this.get(this.getArgument(argRoute[0])); + return (Optional)this.get(this.getArgument(argRoute[0])); } else if ((matchedParsedArgs = this.getSubParsedArgs(argRoute[0])) != null) { return matchedParsedArgs.get(Arrays.copyOfRange(argRoute, 1, argRoute.length)); } else { @@ -127,6 +113,7 @@ public ParsedArgument get(@NotNull String... argRoute) { /** * Returns the argument in {@link #parsedArgs} with the given name. + * * @throws ArgumentNotFoundException If no argument with the given name is found */ private @NotNull Argument getArgument(@NotNull String name) { @@ -140,128 +127,14 @@ public ParsedArgument get(@NotNull String... argRoute) { /** * Returns the sub {@link ParsedArguments} with the given name. If none is found with the given name, returns - * null. + * {@code null}. + * * @param name The name of the sub command - * @return The sub {@link ParsedArguments} with the given name, or null if none is found + * @return The sub {@link ParsedArguments} with the given name, or {@code null} if none is found */ public ParsedArguments getSubParsedArgs(@NotNull String name) { for (var sub : this.subParsedArguments) if (sub.cmd.hasName(name)) return sub; return null; } - - - /** - * Container for a parsed argument value. - * @param The type of the argument value. - */ - public static class ParsedArgument { - private final @Nullable T value; - - ParsedArgument(@Nullable T value) { - this.value = value; - } - - /** - * @return The parsed value of the argument, or null if the argument was not parsed. - */ - public @Nullable T get() { - return this.value; - } - - /** - * @return true if the argument was parsed, false otherwise. - */ - public boolean defined() { - return this.value != null; - } - - /** - * Specifies a function to run if the argument was parsed. - * - * @param onDefined The function to run if the argument was parsed. This function will receive the parsed - * value. - */ - public @NotNull ParsedArgument defined(@NotNull Consumer onDefined) { - if (this.defined()) onDefined.accept(this.value); - return this; - } - - /** - * Returns true if the argument was not parsed, false otherwise. If a single value array is passed, and the - * argument was parsed, this will set the first value of the array to the parsed value. - * @param value A single value array to set the parsed value to if the argument was parsed. - * @return true if the argument was parsed, false otherwise. - * @throws IllegalArgumentException If the value array is not of length 1 - */ - public boolean defined(@Nullable T @NotNull [] value) { - if (value.length != 1) { - throw new IllegalArgumentException("value must be an array of length 1"); - } - - if (this.defined()) { - value[0] = this.value; - return true; - } - - return false; - } - - /** - * @return true if the argument was not parsed, false otherwise. - */ - public boolean undefined() { - return this.value == null; - } - - /** - * Returns the supplied fallback value if the argument was not parsed, otherwise returns the parsed value. - * - * @param fallbackValue The fallback value to return if the argument was not parsed. - * @return The parsed value if the argument was parsed, otherwise the fallback value. - */ - public T undefined(@NotNull T fallbackValue) { - return this.defined() ? this.value : fallbackValue; - } - - /** - * Specifies a supplier function that will be called when the argument is not parsed. The supplier will be - * called and its return value will be returned if so. - * - * @param fallbackCb The supplier function to call if the argument was not parsed. - * @return The parsed value if the argument was parsed, otherwise the value returned by the supplier. - */ - public T undefined(@NotNull Supplier<@NotNull T> fallbackCb) { - return this.defined() ? this.value : fallbackCb.get(); - } - - /** - * Specifies a function to run if the argument was not parsed. - * @param onUndefined The function to run if the argument was not parsed. - */ - public @NotNull ParsedArgument undefined(@NotNull Runnable onUndefined) { - if (this.undefined()) onUndefined.run(); - return this; - } - - /** - * Returns true if the argument was parsed and the value matches the given predicate, false - * otherwise. - * - * @param predicate The predicate to test the value against (if the argument was parsed). This predicate will - * never receive a null value. - * @return true if the argument was parsed and the value matches the given predicate, false otherwise. - */ - public boolean matches(@NotNull Predicate<@Nullable T> predicate) { - return this.defined() && predicate.test(this.value); - } - - /** - * @return A {@link Optional} containing the parsed value if the argument was parsed, or an empty {@link Optional} - * otherwise. - */ - public Optional asOptional() { - return Optional.ofNullable(this.value); - } - } } \ No newline at end of file diff --git a/src/main/java/lanat/ParsedArgumentsRoot.java b/src/main/java/lanat/ParsedArgumentsRoot.java index a063f75d..4bb5e41b 100644 --- a/src/main/java/lanat/ParsedArgumentsRoot.java +++ b/src/main/java/lanat/ParsedArgumentsRoot.java @@ -7,6 +7,10 @@ import java.util.List; import java.util.Optional; +/** + * Container for all the parsed arguments and their respective values. + * Provides methods specific to the root command. + */ public class ParsedArgumentsRoot extends ParsedArguments { private final @Nullable String forwardValue; @@ -23,6 +27,7 @@ public class ParsedArgumentsRoot extends ParsedArguments { /** * Returns the forward value. An empty {@link String} is returned if no forward value was specified. + * The forward value is the string that is passed after the {@code --} token. */ public @NotNull Optional getForwardValue() { return Optional.ofNullable(this.forwardValue); diff --git a/src/main/java/lanat/argumentTypes/BooleanArgument.java b/src/main/java/lanat/argumentTypes/BooleanArgumentType.java similarity index 75% rename from src/main/java/lanat/argumentTypes/BooleanArgument.java rename to src/main/java/lanat/argumentTypes/BooleanArgumentType.java index de26dac7..ec676c09 100644 --- a/src/main/java/lanat/argumentTypes/BooleanArgument.java +++ b/src/main/java/lanat/argumentTypes/BooleanArgumentType.java @@ -6,8 +6,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class BooleanArgument extends ArgumentType { - public BooleanArgument() { +/** + * An argument type that is set in a true state if the argument was used. + */ +public class BooleanArgumentType extends ArgumentType { + public BooleanArgumentType() { super(false); } diff --git a/src/main/java/lanat/argumentTypes/ByteArgumentType.java b/src/main/java/lanat/argumentTypes/ByteArgumentType.java new file mode 100644 index 00000000..c92c3ac4 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/ByteArgumentType.java @@ -0,0 +1,21 @@ +package lanat.argumentTypes; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +/** + * An argument type that takes a byte value. + */ +public class ByteArgumentType extends NumberArgumentType { + @Override + protected @NotNull Function<@NotNull String, @NotNull Byte> getParseFunction() { + return Byte::parseByte; + } + + @Override + public @Nullable String getDescription() { + return "A small integer value. (-128 to 127)"; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/CounterArgument.java b/src/main/java/lanat/argumentTypes/CounterArgumentType.java similarity index 80% rename from src/main/java/lanat/argumentTypes/CounterArgument.java rename to src/main/java/lanat/argumentTypes/CounterArgumentType.java index 53ee9992..d3b11861 100644 --- a/src/main/java/lanat/argumentTypes/CounterArgument.java +++ b/src/main/java/lanat/argumentTypes/CounterArgumentType.java @@ -6,8 +6,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class CounterArgument extends ArgumentType { - public CounterArgument() { +/** + * An argument type that counts the number of times it is used. + */ +public class CounterArgumentType extends ArgumentType { + public CounterArgumentType() { super(0); } diff --git a/src/main/java/lanat/argumentTypes/DoubleArgumentType.java b/src/main/java/lanat/argumentTypes/DoubleArgumentType.java new file mode 100644 index 00000000..89825e46 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/DoubleArgumentType.java @@ -0,0 +1,21 @@ +package lanat.argumentTypes; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +/** + * An argument type that takes a double precision floating point number. + */ +public class DoubleArgumentType extends NumberArgumentType { + @Override + protected @NotNull Function<@NotNull String, @NotNull Double> getParseFunction() { + return Double::parseDouble; + } + + @Override + public @Nullable String getDescription() { + return "A high precision floating point number."; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/DummyArgumentType.java b/src/main/java/lanat/argumentTypes/DummyArgumentType.java new file mode 100644 index 00000000..8fe67340 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/DummyArgumentType.java @@ -0,0 +1,18 @@ +package lanat.argumentTypes; + +import lanat.ArgumentType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * This is a dummy argument type that does not parse any values. It cannot be instantiated, and it's only purpose is to + * be used as a default value for the {@link lanat.Argument.Define} annotation. + */ +public final class DummyArgumentType extends ArgumentType { + private DummyArgumentType() {} + + @Override + public @Nullable Void parseValues(@NotNull String @NotNull [] args) { + return null; + } +} diff --git a/src/main/java/lanat/argumentTypes/EnumArgument.java b/src/main/java/lanat/argumentTypes/EnumArgumentType.java similarity index 75% rename from src/main/java/lanat/argumentTypes/EnumArgument.java rename to src/main/java/lanat/argumentTypes/EnumArgumentType.java index b4ce9bde..31e6d793 100644 --- a/src/main/java/lanat/argumentTypes/EnumArgument.java +++ b/src/main/java/lanat/argumentTypes/EnumArgumentType.java @@ -9,10 +9,18 @@ import java.util.Arrays; -public class EnumArgument> extends ArgumentType { +/** + * An argument type that takes an enum value. + * By supplying a default value in the constructor, the enum type is inferred. + *

    + * The user can specify the enum value by its name, case insensitive. + *

    + * @param The enum type. + */ +public class EnumArgumentType> extends ArgumentType { private final @NotNull T @NotNull [] values; - public EnumArgument(@NotNull T defaultValue) { + public EnumArgumentType(@NotNull T defaultValue) { super(defaultValue); this.values = defaultValue.getDeclaringClass().getEnumConstants(); } @@ -34,9 +42,10 @@ public T parseValues(@NotNull String @NotNull [] args) { for (var i = 0; i < this.values.length; i++) { final var value = this.values[i]; + // if value is the default value, make it bold and yellow if (value == this.getInitialValue()) fmt.concat(new TextFormatter(value.name()) - .setColor(Color.YELLOW) + .withForegroundColor(Color.YELLOW) .addFormat(FormatOption.BOLD) ); else diff --git a/src/main/java/lanat/argumentTypes/FileArgument.java b/src/main/java/lanat/argumentTypes/FileArgumentType.java similarity index 69% rename from src/main/java/lanat/argumentTypes/FileArgument.java rename to src/main/java/lanat/argumentTypes/FileArgumentType.java index 0d9d4069..afcab7c2 100644 --- a/src/main/java/lanat/argumentTypes/FileArgument.java +++ b/src/main/java/lanat/argumentTypes/FileArgumentType.java @@ -6,7 +6,11 @@ import java.io.File; -public class FileArgument extends ArgumentType { +/** + * An argument type that takes a file path, and returns a {@link File} instance. + * If the file could not be found, an error is added. + */ +public class FileArgumentType extends ArgumentType { @Override public File parseValues(@NotNull String @NotNull [] args) { try { diff --git a/src/main/java/lanat/argumentTypes/FloatArgument.java b/src/main/java/lanat/argumentTypes/FloatArgument.java deleted file mode 100644 index 75da8f99..00000000 --- a/src/main/java/lanat/argumentTypes/FloatArgument.java +++ /dev/null @@ -1,28 +0,0 @@ -package lanat.argumentTypes; - -import lanat.ArgumentType; -import lanat.utils.displayFormatter.TextFormatter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class FloatArgument extends ArgumentType { - @Override - public Float parseValues(@NotNull String @NotNull [] args) { - try { - return Float.parseFloat(args[0]); - } catch (NumberFormatException e) { - this.addError("Invalid float value: '" + args[0] + "'."); - return null; - } - } - - @Override - public @NotNull TextFormatter getRepresentation() { - return new TextFormatter("float"); - } - - @Override - public @Nullable String getDescription() { - return "A floating point number."; - } -} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/FloatArgumentType.java b/src/main/java/lanat/argumentTypes/FloatArgumentType.java new file mode 100644 index 00000000..7a240f85 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/FloatArgumentType.java @@ -0,0 +1,21 @@ +package lanat.argumentTypes; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +/** + * An argument type that takes a floating point number. + */ +public class FloatArgumentType extends NumberArgumentType { + @Override + protected @NotNull Function<@NotNull String, @NotNull Float> getParseFunction() { + return Float::parseFloat; + } + + @Override + public @Nullable String getDescription() { + return "A floating point number."; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/FromParseableArgument.java b/src/main/java/lanat/argumentTypes/FromParseableArgument.java deleted file mode 100644 index 521dea9b..00000000 --- a/src/main/java/lanat/argumentTypes/FromParseableArgument.java +++ /dev/null @@ -1,35 +0,0 @@ -package lanat.argumentTypes; - -import lanat.ArgumentType; -import lanat.utils.Range; -import lanat.utils.displayFormatter.TextFormatter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class FromParseableArgument, TInner> extends ArgumentType { - private final @NotNull T parseable; - - public FromParseableArgument(@NotNull T parseable) { - this.parseable = parseable; - } - - @Override - public @Nullable TInner parseValues(@NotNull String @NotNull [] args) { - return this.parseable.parseValues(args); - } - - @Override - public @NotNull Range getRequiredArgValueCount() { - return this.parseable.getRequiredArgValueCount(); - } - - @Override - public @Nullable TextFormatter getRepresentation() { - return this.parseable.getRepresentation(); - } - - @Override - public @Nullable String getDescription() { - return this.parseable.getDescription(); - } -} diff --git a/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java b/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java new file mode 100644 index 00000000..0c41a446 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java @@ -0,0 +1,56 @@ +package lanat.argumentTypes; + +import lanat.ArgumentType; +import lanat.utils.Range; +import lanat.utils.displayFormatter.TextFormatter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An argument type that uses a {@link Parseable} to parse values. If the {@link Parseable#parseValues(String[])} + * method returns {@code null}, an error is added. The error message can be specified in the constructor. + * @param The {@link Parseable} type. + * @param The type of the value returned by the {@link Parseable#parseValues(String[])} method. + */ +public class FromParseableArgumentType, TInner> extends ArgumentType { + private final @NotNull T parseable; + private final @NotNull String errorMessage; + + public FromParseableArgumentType(@NotNull T parseable, @NotNull String errorMessage) { + this.parseable = parseable; + this.errorMessage = errorMessage; + } + + public FromParseableArgumentType(@NotNull T parseable) { + this(parseable, "Invalid value for type " + parseable.getName() + "."); + } + + @Override + public @Nullable TInner parseValues(@NotNull String @NotNull [] args) { + TInner result = this.parseable.parseValues(args); + if (result == null) { + this.addError(this.errorMessage); + } + return result; + } + + @Override + public @NotNull Range getRequiredArgValueCount() { + return this.parseable.getRequiredArgValueCount(); + } + + @Override + public @Nullable TextFormatter getRepresentation() { + return this.parseable.getRepresentation(); + } + + @Override + public @Nullable String getDescription() { + return this.parseable.getDescription(); + } + + @Override + public @NotNull String getName() { + return this.parseable.getName(); + } +} diff --git a/src/main/java/lanat/argumentTypes/IntArgument.java b/src/main/java/lanat/argumentTypes/IntArgument.java deleted file mode 100644 index 30ac4d25..00000000 --- a/src/main/java/lanat/argumentTypes/IntArgument.java +++ /dev/null @@ -1,28 +0,0 @@ -package lanat.argumentTypes; - -import lanat.ArgumentType; -import lanat.utils.displayFormatter.TextFormatter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class IntArgument extends ArgumentType { - @Override - public Integer parseValues(String @NotNull [] args) { - try { - return Integer.parseInt(args[0]); - } catch (NumberFormatException e) { - this.addError("Invalid integer value: '" + args[0] + "'."); - return null; - } - } - - @Override - public @NotNull TextFormatter getRepresentation() { - return new TextFormatter("int"); - } - - @Override - public @Nullable String getDescription() { - return "An integer."; - } -} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/IntRangeArgument.java b/src/main/java/lanat/argumentTypes/IntRangeArgument.java deleted file mode 100644 index 548c9cac..00000000 --- a/src/main/java/lanat/argumentTypes/IntRangeArgument.java +++ /dev/null @@ -1,44 +0,0 @@ -package lanat.argumentTypes; - -import lanat.utils.displayFormatter.Color; -import lanat.utils.displayFormatter.TextFormatter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class IntRangeArgument extends IntArgument { - private final int min, max; - - public IntRangeArgument(int min, int max) { - if (min > max) { - throw new IllegalArgumentException("min must be less than or equal to max"); - } - - this.min = min; - this.max = max; - } - - @Override - public Integer parseValues(String @NotNull [] args) { - var result = super.parseValues(args); - - if (result == null) return null; - - if (result < this.min || result > this.max) { - this.addError("Value must be between " + this.min + " and " + this.max + "."); - return null; - } - - return result; - } - - @Override - public @NotNull TextFormatter getRepresentation() { - return super.getRepresentation() - .concat(new TextFormatter("[%d-%d]".formatted(this.min, this.max)).setColor(Color.YELLOW)); - } - - @Override - public @Nullable String getDescription() { - return super.getDescription() + " Must be between " + this.min + " and " + this.max + ". (Inclusive)"; - } -} diff --git a/src/main/java/lanat/argumentTypes/IntegerArgumentType.java b/src/main/java/lanat/argumentTypes/IntegerArgumentType.java new file mode 100644 index 00000000..52637a41 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/IntegerArgumentType.java @@ -0,0 +1,21 @@ +package lanat.argumentTypes; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +/** + * An argument type that takes an integer number. + */ +public class IntegerArgumentType extends NumberArgumentType { + @Override + protected @NotNull Function getParseFunction() { + return Integer::parseInt; + } + + @Override + public @Nullable String getDescription() { + return "An integer number."; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/KeyValuesArgument.java b/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java similarity index 68% rename from src/main/java/lanat/argumentTypes/KeyValuesArgument.java rename to src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java index 82ce4bda..292d01c0 100644 --- a/src/main/java/lanat/argumentTypes/KeyValuesArgument.java +++ b/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java @@ -2,6 +2,7 @@ import lanat.ArgumentType; import lanat.utils.Range; +import lanat.utils.UtlString; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -9,23 +10,26 @@ import java.util.HashMap; import java.util.Objects; -public class KeyValuesArgument, Ts> extends ArgumentType> { +/** + * An argument type that takes key-value pairs. The key is a string and the value is of another type that is specified + * in the constructor. + *

    + * The final value of this argument type is a {@link HashMap} of the key-value pairs. + *

    + * @param The type of the argument type used to parse the values. + * @param The type of the values. + */ +public class KeyValuesArgumentType, Ts> extends ArgumentType> { private final @NotNull ArgumentType valueType; - private final char separator; - public KeyValuesArgument(@NotNull T type, char separator) { + public KeyValuesArgumentType(@NotNull T type) { if (type.getRequiredArgValueCount().min() != 1) throw new IllegalArgumentException("The value type must at least accept one value."); this.valueType = type; - this.separator = separator; this.registerSubType(type); } - public KeyValuesArgument(@NotNull T type) { - this(type, '='); - } - @Override public @NotNull Range getRequiredArgValueCount() { return Range.AT_LEAST_ONE; @@ -36,15 +40,15 @@ public KeyValuesArgument(@NotNull T type) { HashMap tempHashMap = new HashMap<>(); this.forEachArgValue(args, arg -> { - final var split = arg.split("\\%c".formatted(this.separator)); + final var split = UtlString.split(arg, '='); if (split.length != 2) { this.addError("Invalid key-value pair: '" + arg + "'."); return; } - final var key = split[0].strip(); - final var value = split[1].strip(); + final var key = split[0]; + final var value = split[1]; if (key.isEmpty()) { this.addError("Key cannot be empty."); @@ -56,8 +60,7 @@ public KeyValuesArgument(@NotNull T type) { return; } - this.valueType.parseAndUpdateValue(value); - tempHashMap.put(key, this.valueType.getFinalValue()); + tempHashMap.put(key, this.valueType.parseValues(value)); }); if (tempHashMap.isEmpty()) @@ -77,8 +80,4 @@ public KeyValuesArgument(@NotNull T type) { public @Nullable String getDescription() { return "A list of key-value pairs. The key must be a string and the value must be of type " + this.valueType.getName() + "."; } - - public static , Ts> KeyValuesArgument create(T type, char separator) { - return new KeyValuesArgument<>(type, separator); - } } diff --git a/src/main/java/lanat/argumentTypes/LongArgumentType.java b/src/main/java/lanat/argumentTypes/LongArgumentType.java new file mode 100644 index 00000000..bef388e8 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/LongArgumentType.java @@ -0,0 +1,21 @@ +package lanat.argumentTypes; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +/** + * An argument type that takes a long integer number. + */ +public class LongArgumentType extends NumberArgumentType { + @Override + protected @NotNull Function<@NotNull String, @NotNull Long> getParseFunction() { + return Long::parseLong; + } + + @Override + public @Nullable String getDescription() { + return "A large integer number."; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/MultipleStringsArgument.java b/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java similarity index 78% rename from src/main/java/lanat/argumentTypes/MultipleStringsArgument.java rename to src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java index ef90d0e0..291c46b4 100644 --- a/src/main/java/lanat/argumentTypes/MultipleStringsArgument.java +++ b/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java @@ -5,7 +5,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class MultipleStringsArgument extends ArgumentType { +/** + * An argument type that takes multiple strings. + */ +public class MultipleStringsArgumentType extends ArgumentType { @Override public @NotNull Range getRequiredArgValueCount() { return Range.AT_LEAST_ONE; diff --git a/src/main/java/lanat/argumentTypes/NumberArgumentType.java b/src/main/java/lanat/argumentTypes/NumberArgumentType.java new file mode 100644 index 00000000..08da05cd --- /dev/null +++ b/src/main/java/lanat/argumentTypes/NumberArgumentType.java @@ -0,0 +1,24 @@ +package lanat.argumentTypes; + +import lanat.ArgumentType; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +public abstract class NumberArgumentType extends ArgumentType { + /** + * Returns the function that will parse a string as a number. e.g. {@link Integer#parseInt(String)}. + * @return The function that will parse a string as a number. + */ + protected abstract @NotNull Function<@NotNull String, @NotNull T> getParseFunction(); + + @Override + public T parseValues(@NotNull String @NotNull [] args) { + try { + return this.getParseFunction().apply(args[0]); + } catch (NumberFormatException e) { + this.addError("Invalid " + this.getName() + " value: '" + args[0] + "'."); + return null; + } + } +} diff --git a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java new file mode 100644 index 00000000..3c0cf5b7 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java @@ -0,0 +1,59 @@ +package lanat.argumentTypes; + +import lanat.ArgumentType; +import lanat.utils.UtlReflection; +import lanat.utils.displayFormatter.Color; +import lanat.utils.displayFormatter.TextFormatter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class NumberRangeArgumentType> extends ArgumentType { + private final ArgumentType argumentType; + private final T min, max; + + @SuppressWarnings("unchecked") + public NumberRangeArgumentType(@NotNull T min, @NotNull T max) { + if (min.compareTo(max) > 0) { + throw new IllegalArgumentException("min must be less than or equal to max"); + } + + final var typeInferred = ArgumentType.getTypeInfer(min.getClass()); + + if (typeInferred == null) { + throw new IllegalArgumentException("Unsupported type: " + min.getClass().getName()); + } + + this.argumentType = (ArgumentType)UtlReflection.instantiate(typeInferred); + this.registerSubType(this.argumentType); + + this.min = min; + this.max = max; + } + + @Override + public @Nullable T parseValues(@NotNull String... args) { + var result = this.argumentType.parseValues(args); + + if (result == null) return null; + + if (result.compareTo(this.min) < 0 || result.compareTo(this.max) > 0) { + this.addError("Value must be between " + this.min + " and " + this.max + "."); + return null; + } + + return result; + } + + @Override + public @NotNull TextFormatter getRepresentation() { + return Objects.requireNonNull(this.argumentType.getRepresentation()) + .concat(new TextFormatter("[%s-%s]".formatted(this.min, this.max)).withForegroundColor(Color.YELLOW)); + } + + @Override + public @Nullable String getDescription() { + return this.argumentType.getDescription() + " Must be between " + this.min + " and " + this.max + ". (Inclusive)"; + } +} diff --git a/src/main/java/lanat/argumentTypes/Parseable.java b/src/main/java/lanat/argumentTypes/Parseable.java index ecadc05b..0fc59f20 100644 --- a/src/main/java/lanat/argumentTypes/Parseable.java +++ b/src/main/java/lanat/argumentTypes/Parseable.java @@ -6,21 +6,39 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.regex.Pattern; + +/** + * The basic interface for all argument types. In order to use a class that implements this interface as an + * argument type, you must use {@link FromParseableArgumentType} to wrap it. + * @param The type that this argument type parses. + * @see FromParseableArgumentType + */ public interface Parseable extends NamedWithDescription { + Pattern DEFAULT_NAME_REGEX = Pattern.compile("ArgumentType$", Pattern.CASE_INSENSITIVE); + + + /** Specifies the number of values that this parser should receive when calling {@link #parseValues(String[])}. */ @NotNull Range getRequiredArgValueCount(); - @Nullable T parseValues(@NotNull String @NotNull [] args); + /** + * Parses the received values and returns the result. If the values are invalid, this method shall return {@code null}. + * + * @param args The values that were received. + * @return The parsed value. + */ + @Nullable T parseValues(@NotNull String... args); + /** Returns the representation of this parseable type. This may appear in places like the help message. */ default @Nullable TextFormatter getRepresentation() { - return null; + return new TextFormatter(this.getName()); } @Override default @NotNull String getName() { - return this.getClass() - .getSimpleName() - .toLowerCase() - .replaceAll("argument$", ""); + return Parseable.DEFAULT_NAME_REGEX + .matcher(this.getClass().getSimpleName()) + .replaceAll(""); } @Override diff --git a/src/main/java/lanat/argumentTypes/ShortArgumentType.java b/src/main/java/lanat/argumentTypes/ShortArgumentType.java new file mode 100644 index 00000000..529d419c --- /dev/null +++ b/src/main/java/lanat/argumentTypes/ShortArgumentType.java @@ -0,0 +1,21 @@ +package lanat.argumentTypes; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +/** + * An argument type that takes a short integer number. + */ +public class ShortArgumentType extends NumberArgumentType { + @Override + protected @NotNull Function<@NotNull String, @NotNull Short> getParseFunction() { + return Short::parseShort; + } + + @Override + public @Nullable String getDescription() { + return "An integer number (-32,768 to 32,767)"; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/StdinArgument.java b/src/main/java/lanat/argumentTypes/StdinArgumentType.java similarity index 50% rename from src/main/java/lanat/argumentTypes/StdinArgument.java rename to src/main/java/lanat/argumentTypes/StdinArgumentType.java index df5a1c32..b51e2514 100644 --- a/src/main/java/lanat/argumentTypes/StdinArgument.java +++ b/src/main/java/lanat/argumentTypes/StdinArgumentType.java @@ -6,15 +6,13 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; - -public class StdinArgument extends ArgumentType { - private final BufferedReader systemIn = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); +import java.util.Scanner; +/** + * An argument type that takes input from stdin (Standard Input). + * This waits for the user to input something. May be useful for piping input from other programs. + */ +public class StdinArgumentType extends ArgumentType { @Override public @NotNull Range getRequiredArgValueCount() { return Range.NONE; @@ -27,15 +25,15 @@ public TextFormatter getRepresentation() { @Override public String parseValues(@NotNull String @NotNull [] args) { - ArrayList input = new ArrayList<>(); + final var input = new StringBuilder(); - try { - String line; - while ((line = this.systemIn.readLine()) != null) - input.add(line); - } catch (IOException ignored) {} + try (var scanner = new Scanner(System.in)) { + while (scanner.hasNextLine()) { + input.append(scanner.nextLine()).append('\n'); + } + } - return String.join("\n", input); + return input.toString(); } @Override diff --git a/src/main/java/lanat/argumentTypes/StringArgument.java b/src/main/java/lanat/argumentTypes/StringArgumentType.java similarity index 79% rename from src/main/java/lanat/argumentTypes/StringArgument.java rename to src/main/java/lanat/argumentTypes/StringArgumentType.java index 9a254079..1f04a3c0 100644 --- a/src/main/java/lanat/argumentTypes/StringArgument.java +++ b/src/main/java/lanat/argumentTypes/StringArgumentType.java @@ -5,7 +5,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class StringArgument extends ArgumentType { +/** + * An argument type that takes a string of characters. + */ +public class StringArgumentType extends ArgumentType { @Override public String parseValues(@NotNull String @NotNull [] args) { return args[0]; diff --git a/src/main/java/lanat/argumentTypes/TryParseArgument.java b/src/main/java/lanat/argumentTypes/TryParseArgument.java deleted file mode 100644 index 8c8e6d3e..00000000 --- a/src/main/java/lanat/argumentTypes/TryParseArgument.java +++ /dev/null @@ -1,91 +0,0 @@ -package lanat.argumentTypes; - -import fade.mirror.Invokable; -import fade.mirror.MClass; -import fade.mirror.MMethod; -import fade.mirror.Parameterized; -import fade.mirror.exception.MirrorException; -import lanat.ArgumentType; -import lanat.exceptions.ArgumentTypeException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Function; - -import static fade.mirror.Mirror.mirror; - -public class TryParseArgument extends ArgumentType { - private final Function parseMethod; - private final @NotNull MClass type; - - - public TryParseArgument(@NotNull Class type) { - this.type = mirror(type); - - if ((this.parseMethod = this.getParseMethod()) == null) - throw new ArgumentTypeException( - "Type " + type.getName() + " must have a static valueOf(String), parse(String), " - + "or from(String) method, or a constructor that takes a string." - ); - } - - private > boolean isValidMethod(I method) { - if (method.getParameterCount() != 1) return false; - if (method.getReturnType() != this.type.getRawClass()) return false; - return method.getParameter(parameter -> parameter.getType().equals(String.class)).isPresent(); - } - - @Override - protected void addError(@NotNull String value) { - super.addError("Unable to parse value '" + value + "' as type " + this.type.getSimpleName() + "."); - } - - private @Nullable Function getParseMethod() { - // Get a static valueOf(String), a parse(String), or a from(String) method. - final var method = this.type.getMethods() - .filter(MMethod::isStatic) - .filter(this::isValidMethod) - .filter(m -> (m.getName().equals("valueOf") || m.getName().equals("from") || m.getName().equals("parse"))) - .findFirst(); - - // if we found a method, return that. - if (method.isPresent()) { - return input -> { - try { - return method.get().invoke(input); - } catch (MirrorException exception) { - this.addError(input); - } - return null; - }; - } - - // Otherwise, try to find a constructor that takes a string. - return this.type.getConstructors() - .filter(this::isValidMethod) - .findFirst() - .>map(constructor -> s -> { - try { - return constructor.invoke(s); - } catch (MirrorException exception) { - this.addError(s); - } - return null; - }).orElse(null); - } - - @Override - @SuppressWarnings("unchecked") - public @Nullable T parseValues(@NotNull String @NotNull [] args) { - try { - return (T) this.parseMethod.apply(args[0]); - } catch (Exception e) { - throw new ArgumentTypeException("Unable to cast value '" + args[0] + "' to type " + this.type.getSimpleName() + ".", e); - } - } - - @Override - public @Nullable String getDescription() { - return "A value of type " + this.type.getSimpleName() + "."; - } -} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/TryParseArgumentType.java b/src/main/java/lanat/argumentTypes/TryParseArgumentType.java new file mode 100644 index 00000000..f4ba5747 --- /dev/null +++ b/src/main/java/lanat/argumentTypes/TryParseArgumentType.java @@ -0,0 +1,105 @@ +package lanat.argumentTypes; + +import lanat.ArgumentType; +import lanat.exceptions.ArgumentTypeException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Executable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * An argument type that attempts to parse a string into the type given in the constructor. + *

    + * The type given must have a static {@code valueOf(String)}, {@code parse(String)}, or {@code from(String)} method, + * or a constructor that takes a string. If none of these are found, an exception will be thrown. + *

    + * @param The type to parse the string into. + */ +public class TryParseArgumentType extends ArgumentType { + private final Function parseMethod; + private final @NotNull Class type; + private static final String[] TRY_PARSE_METHOD_NAMES = new String[] { "valueOf", "from", "parse" }; + + + public TryParseArgumentType(@NotNull Class type) { + this.type = type; + + if ((this.parseMethod = this.getParseMethod()) == null) + throw new ArgumentTypeException( + "Type " + type.getName() + " must have a static valueOf(String), parse(String), " + + "or from(String) method, or a constructor that takes a string." + ); + } + + private static boolean isValidExecutable(Executable executable) { + return Modifier.isStatic(executable.getModifiers()) + && executable.getParameterCount() == 1 + && executable.getParameterTypes()[0] == String.class + && Arrays.asList(TryParseArgumentType.TRY_PARSE_METHOD_NAMES).contains(executable.getName()); + } + + private boolean isValidMethod(Method method) { + return TryParseArgumentType.isValidExecutable(method) + && method.getReturnType() == this.type; + } + + + @Override + protected void addError(@NotNull String value) { + super.addError("Unable to parse value '" + value + "' as type " + this.type.getSimpleName() + "."); + } + + private @Nullable Function getParseMethod() { + // Get a static valueOf(String), a parse(String), or a from(String) method. + final var method = Stream.of(this.type.getMethods()) + .filter(this::isValidMethod) + .findFirst(); + + // if we found a method, return that. + if (method.isPresent()) { + return input -> { + try { + return method.get().invoke(null, input); + } catch (IllegalAccessException | InvocationTargetException exception) { + this.addError(input); + } + return null; + }; + } + + // Otherwise, try to find a constructor that takes a string. + final var ctor = Stream.of(this.type.getConstructors()) + .filter(TryParseArgumentType::isValidExecutable) + .findFirst(); + + return ctor.>map(tConstructor -> input -> { + try { + return tConstructor.newInstance(input); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + this.addError(input); + } + return null; + }).orElse(null); + } + + @Override + @SuppressWarnings("unchecked") + public @Nullable T parseValues(@NotNull String @NotNull [] args) { + try { + return (T)this.parseMethod.apply(args[0]); + } catch (Exception e) { + throw new ArgumentTypeException("Unable to cast value '" + args[0] + "' to type " + this.type.getSimpleName() + ".", e); + } + } + + @Override + public @Nullable String getDescription() { + return "A value of type " + this.type.getSimpleName() + "."; + } +} \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/TupleArgumentType.java b/src/main/java/lanat/argumentTypes/TupleArgumentType.java index 27d0fd9b..4b95155a 100644 --- a/src/main/java/lanat/argumentTypes/TupleArgumentType.java +++ b/src/main/java/lanat/argumentTypes/TupleArgumentType.java @@ -7,7 +7,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; - +/** + * Provides a base for argument types that take multiple values. + * Shows a properly formatted description and representation. + * @param the type of the value that the argument will take + */ public abstract class TupleArgumentType extends ArgumentType { private final @NotNull Range argCount; @@ -24,7 +28,7 @@ public TupleArgumentType(@NotNull Range range, @NotNull T initialValue) { @Override public @NotNull TextFormatter getRepresentation() { return new TextFormatter(this.getValue().getClass().getSimpleName()) - .concat(new TextFormatter(this.argCount.getRegexRange()).setColor(Color.BRIGHT_YELLOW)); + .concat(new TextFormatter(this.argCount.getRegexRange()).withForegroundColor(Color.BRIGHT_YELLOW)); } @Override diff --git a/src/main/java/lanat/commandTemplates/DefaultCommandTemplate.java b/src/main/java/lanat/commandTemplates/DefaultCommandTemplate.java deleted file mode 100644 index d4b75d8c..00000000 --- a/src/main/java/lanat/commandTemplates/DefaultCommandTemplate.java +++ /dev/null @@ -1,28 +0,0 @@ -package lanat.commandTemplates; - -import lanat.Argument; -import lanat.ArgumentParser; -import lanat.CommandTemplate; -import lanat.argumentTypes.BooleanArgument; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class DefaultCommandTemplate extends CommandTemplate { - @ArgDef - public @NotNull Argument help() { - return Argument.create("help") - .onOk(t -> System.out.println(this.cmd().getHelp())) - .description("Shows this message.") - .allowUnique(); - } - - @ArgDef - public @Nullable Argument version() { - return this.cmd() instanceof ArgumentParser ap - ? Argument.create("version") - .onOk(t -> System.out.println("Version: " + ap.getVersion())) - .description("Shows the version of this program.") - .allowUnique() - : null; - } -} diff --git a/src/main/java/lanat/exceptions/ArgumentAlreadyExistsException.java b/src/main/java/lanat/exceptions/ArgumentAlreadyExistsException.java index a318c05b..4049b633 100644 --- a/src/main/java/lanat/exceptions/ArgumentAlreadyExistsException.java +++ b/src/main/java/lanat/exceptions/ArgumentAlreadyExistsException.java @@ -1,16 +1,17 @@ package lanat.exceptions; import lanat.Argument; -import lanat.ArgumentGroupAdder; +import lanat.ArgumentAdder; import lanat.NamedWithDescription; +import org.jetbrains.annotations.NotNull; /** - * Thrown when an {@link Argument} is added to a container that - * already contains an {@link Argument} with the same name. - * */ + * Thrown when an {@link Argument} is added to a container that already contains an {@link Argument} with the same + * name. + */ public class ArgumentAlreadyExistsException extends ObjectAlreadyExistsException { - public - ArgumentAlreadyExistsException(Argument argument, T container) { - super(argument, container); + public + ArgumentAlreadyExistsException(@NotNull Argument argument, @NotNull T container) { + super("Argument", argument, container); } } diff --git a/src/main/java/lanat/exceptions/ArgumentGroupAlreadyExistsException.java b/src/main/java/lanat/exceptions/ArgumentGroupAlreadyExistsException.java index 6fc201a8..f9a4977f 100644 --- a/src/main/java/lanat/exceptions/ArgumentGroupAlreadyExistsException.java +++ b/src/main/java/lanat/exceptions/ArgumentGroupAlreadyExistsException.java @@ -3,14 +3,15 @@ import lanat.ArgumentGroup; import lanat.ArgumentGroupAdder; import lanat.NamedWithDescription; +import org.jetbrains.annotations.NotNull; /** - * Thrown when an {@link ArgumentGroup} is added to a container that already contains - * an {@link ArgumentGroup} with the same name. - * */ + * Thrown when an {@link ArgumentGroup} is added to a container that already contains an {@link ArgumentGroup} with the + * same name. + */ public class ArgumentGroupAlreadyExistsException extends ObjectAlreadyExistsException { public - ArgumentGroupAlreadyExistsException(final ArgumentGroup group, final T container) { - super(group, container); + ArgumentGroupAlreadyExistsException(@NotNull ArgumentGroup group, final T container) { + super("Group", group, container); } } diff --git a/src/main/java/lanat/exceptions/ArgumentGroupNotFoundException.java b/src/main/java/lanat/exceptions/ArgumentGroupNotFoundException.java new file mode 100644 index 00000000..87b5ed21 --- /dev/null +++ b/src/main/java/lanat/exceptions/ArgumentGroupNotFoundException.java @@ -0,0 +1,15 @@ +package lanat.exceptions; + +import lanat.ArgumentGroup; +import org.jetbrains.annotations.NotNull; + +/** Thrown when an {@link ArgumentGroup} is not found. */ +public class ArgumentGroupNotFoundException extends ObjectNotFoundException { + public ArgumentGroupNotFoundException(@NotNull String name) { + super("Group", name); + } + + public ArgumentGroupNotFoundException(@NotNull ArgumentGroup group) { + super("Group", group); + } +} diff --git a/src/main/java/lanat/exceptions/ArgumentNotFoundException.java b/src/main/java/lanat/exceptions/ArgumentNotFoundException.java index 8884de5b..e6eb332e 100644 --- a/src/main/java/lanat/exceptions/ArgumentNotFoundException.java +++ b/src/main/java/lanat/exceptions/ArgumentNotFoundException.java @@ -1,14 +1,20 @@ package lanat.exceptions; import lanat.Argument; +import lanat.NamedWithDescription; +import org.jetbrains.annotations.NotNull; /** Thrown when an {@link Argument} is not found. */ -public class ArgumentNotFoundException extends LanatException { - public ArgumentNotFoundException(Argument argument) { - this(argument.getName()); +public class ArgumentNotFoundException extends ObjectNotFoundException { + public ArgumentNotFoundException(@NotNull Argument argument) { + super("Argument", argument); } - public ArgumentNotFoundException(String name) { - super("Argument not found: " + name); + public ArgumentNotFoundException(@NotNull String name) { + super("Argument", name); + } + + public ArgumentNotFoundException(@NotNull Argument argument, @NotNull NamedWithDescription container) { + super("Argument", argument, container); } } diff --git a/src/main/java/lanat/exceptions/ArgumentTypeException.java b/src/main/java/lanat/exceptions/ArgumentTypeException.java index 9fa6beb1..5a71fa70 100644 --- a/src/main/java/lanat/exceptions/ArgumentTypeException.java +++ b/src/main/java/lanat/exceptions/ArgumentTypeException.java @@ -1,11 +1,14 @@ package lanat.exceptions; +import org.jetbrains.annotations.NotNull; + /** Thrown when an error occurs in an {@link lanat.ArgumentType}. */ public class ArgumentTypeException extends LanatException { - public ArgumentTypeException(String message, Throwable cause) { + public ArgumentTypeException(@NotNull String message, @NotNull Throwable cause) { super(message, cause); } - public ArgumentTypeException(String message) { + + public ArgumentTypeException(@NotNull String message) { super(message); } } diff --git a/src/main/java/lanat/exceptions/CommandAlreadyExistsException.java b/src/main/java/lanat/exceptions/CommandAlreadyExistsException.java index 00104ba9..99905751 100644 --- a/src/main/java/lanat/exceptions/CommandAlreadyExistsException.java +++ b/src/main/java/lanat/exceptions/CommandAlreadyExistsException.java @@ -1,10 +1,11 @@ package lanat.exceptions; import lanat.Command; +import org.jetbrains.annotations.NotNull; /** Thrown when a {@link Command} is added to a container that already contains a {@link Command} with the same name. */ public class CommandAlreadyExistsException extends ObjectAlreadyExistsException { - public CommandAlreadyExistsException(Command command, Command container) { - super(command, container); + public CommandAlreadyExistsException(@NotNull Command command, @NotNull Command container) { + super("Command", command, container); } } diff --git a/src/main/java/lanat/exceptions/CommandNotFoundException.java b/src/main/java/lanat/exceptions/CommandNotFoundException.java index f570dd72..3f42c4a5 100644 --- a/src/main/java/lanat/exceptions/CommandNotFoundException.java +++ b/src/main/java/lanat/exceptions/CommandNotFoundException.java @@ -1,8 +1,19 @@ package lanat.exceptions; +import lanat.NamedWithDescription; +import org.jetbrains.annotations.NotNull; + /** Thrown when a {@link lanat.Command} is not found. */ -public class CommandNotFoundException extends LanatException { - public CommandNotFoundException(String name) { - super("Command not found: " + name); +public class CommandNotFoundException extends ObjectNotFoundException { + public CommandNotFoundException(@NotNull String name) { + super("Command", name); + } + + public CommandNotFoundException(@NotNull NamedWithDescription name, @NotNull NamedWithDescription container) { + super("Command", name, container); + } + + public CommandNotFoundException(@NotNull NamedWithDescription name) { + super("Command", name); } } diff --git a/src/main/java/lanat/exceptions/CommandTemplateException.java b/src/main/java/lanat/exceptions/CommandTemplateException.java new file mode 100644 index 00000000..51a5d791 --- /dev/null +++ b/src/main/java/lanat/exceptions/CommandTemplateException.java @@ -0,0 +1,10 @@ +package lanat.exceptions; + +import org.jetbrains.annotations.NotNull; + +/** Thrown when an error occurs while parsing a {@link lanat.CommandTemplate}. */ +public class CommandTemplateException extends LanatException { + public CommandTemplateException(@NotNull String message) { + super(message); + } +} diff --git a/src/main/java/lanat/exceptions/IncompatibleCommandTemplateType.java b/src/main/java/lanat/exceptions/IncompatibleCommandTemplateType.java new file mode 100644 index 00000000..d466d2d7 --- /dev/null +++ b/src/main/java/lanat/exceptions/IncompatibleCommandTemplateType.java @@ -0,0 +1,11 @@ +package lanat.exceptions; + +/** + * Thrown when a type of field inside a {@link lanat.CommandTemplate} is incompatible with the type returned by the + * argument type inner value. + */ +public class IncompatibleCommandTemplateType extends CommandTemplateException { + public IncompatibleCommandTemplateType(String message) { + super(message); + } +} diff --git a/src/main/java/lanat/exceptions/LanatException.java b/src/main/java/lanat/exceptions/LanatException.java index 32453b3a..51f31225 100644 --- a/src/main/java/lanat/exceptions/LanatException.java +++ b/src/main/java/lanat/exceptions/LanatException.java @@ -1,12 +1,14 @@ package lanat.exceptions; +import org.jetbrains.annotations.NotNull; + /** Thrown when an error occurs in Lanat. All Lanat exceptions extend this class. */ public class LanatException extends RuntimeException { - public LanatException(String message) { + public LanatException(@NotNull String message) { super(message); } - public LanatException(String message, Throwable cause) { + public LanatException(@NotNull String message, @NotNull Throwable cause) { super(message, cause); } } diff --git a/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java b/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java index aafff919..b115b2a8 100644 --- a/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java +++ b/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java @@ -3,15 +3,20 @@ import lanat.NamedWithDescription; import lanat.utils.UtlReflection; import lanat.utils.UtlString; +import org.jetbrains.annotations.NotNull; /** - * Thrown when an object is added to a container that - * already contains an object equal to the added one. - * */ -class ObjectAlreadyExistsException extends LanatException { - public ObjectAlreadyExistsException(NamedWithDescription obj, NamedWithDescription container) { + * Thrown when an object is added to a container that already contains an object equal to the added one. + */ +public abstract class ObjectAlreadyExistsException extends LanatException { + public ObjectAlreadyExistsException( + @NotNull String typeName, + @NotNull NamedWithDescription obj, + @NotNull NamedWithDescription container + ) + { super( - UtlReflection.getSimpleName(obj.getClass()) + typeName + " " + UtlString.surround(obj.getName()) + " already exists in " diff --git a/src/main/java/lanat/exceptions/ObjectNotFoundException.java b/src/main/java/lanat/exceptions/ObjectNotFoundException.java new file mode 100644 index 00000000..8b888995 --- /dev/null +++ b/src/main/java/lanat/exceptions/ObjectNotFoundException.java @@ -0,0 +1,38 @@ +package lanat.exceptions; + +import lanat.NamedWithDescription; +import lanat.utils.UtlReflection; +import lanat.utils.UtlString; +import org.jetbrains.annotations.NotNull; + +/** + * Thrown when an object is not found. + */ +public class ObjectNotFoundException extends LanatException { + public ObjectNotFoundException( + @NotNull String typeName, + @NotNull NamedWithDescription obj, + @NotNull NamedWithDescription container + ) + { + super( + typeName + + " " + + UtlString.surround(obj.getName()) + + " was not found in " + + UtlReflection.getSimpleName(container.getClass()) + + " " + + UtlString.surround(container.getName()) + ); + } + + public ObjectNotFoundException(@NotNull String typeName, @NotNull NamedWithDescription obj) { + this(typeName, obj.getName()); + } + + public ObjectNotFoundException(@NotNull String typeName, @NotNull String name) { + super( + typeName + " " + UtlString.surround(name) + " was not found" + ); + } +} diff --git a/src/main/java/lanat/helpRepresentation/ArgumentGroupRepr.java b/src/main/java/lanat/helpRepresentation/ArgumentGroupRepr.java index 7fe527be..2e4ee8e9 100644 --- a/src/main/java/lanat/helpRepresentation/ArgumentGroupRepr.java +++ b/src/main/java/lanat/helpRepresentation/ArgumentGroupRepr.java @@ -22,6 +22,7 @@ private ArgumentGroupRepr() {} * <name>: * <description> * + * * @param group the group * @return the name and description of the group */ @@ -30,7 +31,7 @@ private ArgumentGroupRepr() {} if (description == null) return null; - final var name = new TextFormatter(group.name + ':').addFormat(FormatOption.BOLD); + final var name = new TextFormatter(group.getName() + ':').addFormat(FormatOption.BOLD); if (group.isExclusive()) name.addFormat(FormatOption.UNDERLINE); @@ -47,13 +48,14 @@ private ArgumentGroupRepr() {} * * <subgroup descriptions> * + * * @param group the group * @return the descriptions of the arguments and subgroups of the group */ public static @NotNull String getDescriptions(@NotNull ArgumentGroup group) { final var arguments = Argument.sortByPriority(group.getArguments()); final var buff = new StringBuilder(); - final var name = new TextFormatter(group.name + ':').addFormat(FormatOption.BOLD); + final var name = new TextFormatter(group.getName() + ':').addFormat(FormatOption.BOLD); final var description = DescriptionFormatter.parse(group); final var argumentDescriptions = ArgumentRepr.getDescriptions(arguments); @@ -68,7 +70,7 @@ private ArgumentGroupRepr() {} buff.append(ArgumentRepr.getDescriptions(arguments)); - for (final var subGroup : group.getSubGroups()) { + for (final var subGroup : group.getGroups()) { buff.append(ArgumentGroupRepr.getDescriptions(subGroup)); } @@ -81,6 +83,7 @@ private ArgumentGroupRepr() {} *
     	 * <name> <arguments>
     	 * 
    + * * @param group the group */ public static String getRepresentation(@NotNull ArgumentGroup group) { @@ -105,7 +108,7 @@ public static String getRepresentation(@NotNull ArgumentGroup group) { } } - final List groups = group.getSubGroups().stream().filter(g -> !g.isEmpty()).toList(); + final List groups = group.getGroups().stream().filter(g -> !g.isEmpty()).toList(); if (!arguments.isEmpty() && !groups.isEmpty()) { sb.append(' '); diff --git a/src/main/java/lanat/helpRepresentation/ArgumentRepr.java b/src/main/java/lanat/helpRepresentation/ArgumentRepr.java index 526652ed..9afef39e 100644 --- a/src/main/java/lanat/helpRepresentation/ArgumentRepr.java +++ b/src/main/java/lanat/helpRepresentation/ArgumentRepr.java @@ -24,6 +24,7 @@ private ArgumentRepr() {} * or *

    * {@code ()}: if the argument is positional + * * @param arg the argument * @return the representation of the argument */ @@ -38,12 +39,12 @@ private ArgumentRepr() {} outText.addFormat(FormatOption.BOLD, FormatOption.UNDERLINE); } - outText.setColor(arg.getRepresentationColor()); + outText.withForegroundColor(arg.getRepresentationColor()); if (arg.isPositional() && repr != null) { outText.concat(repr, new TextFormatter("(" + names + ")")); } else { - outText.setContents("" + argPrefix + (names.length() > 1 ? argPrefix : "") + names + (repr == null ? "" : " ")); + outText.withContents("" + argPrefix + (names.length() > 1 ? argPrefix : "") + names + (repr == null ? "" : " ")); if (repr != null) outText.concat(repr); @@ -58,6 +59,7 @@ private ArgumentRepr() {} * <representation>: * <description> * + * * @param arg the argument * @return the representation and description of the argument */ @@ -79,6 +81,7 @@ private ArgumentRepr() {} * * ... * + * * @param arguments the arguments * @return the descriptions of the arguments */ diff --git a/src/main/java/lanat/helpRepresentation/CommandRepr.java b/src/main/java/lanat/helpRepresentation/CommandRepr.java index 42145577..5d2d31b4 100644 --- a/src/main/java/lanat/helpRepresentation/CommandRepr.java +++ b/src/main/java/lanat/helpRepresentation/CommandRepr.java @@ -18,12 +18,13 @@ private CommandRepr() {} *

     	 * {<sub-command1> | <sub-command2> | ...}
     	 * 
    + * * @param cmd the command * @return the representation of the sub-commands of the command */ public static @NotNull String getSubCommandsRepresentation(@NotNull Command cmd) { return '{' - + String.join(" | ", cmd.getSubCommands().stream().map(CommandRepr::getRepresentation).toList()) + + String.join(" | ", cmd.getCommands().stream().map(CommandRepr::getRepresentation).toList()) + '}'; } @@ -33,6 +34,7 @@ private CommandRepr() {} * {@code } *

    * The names are separated by a slash. + * * @param cmd the command * @return the representation of the command */ @@ -45,6 +47,7 @@ private CommandRepr() {} /** * Returns the parsed description of the given command. + * * @param cmd the command * @return the parsed description of the command */ @@ -64,11 +67,12 @@ private CommandRepr() {} * * ... * + * * @param cmd the command * @return the name and representation of the sub-commands of the command */ public static @Nullable String getSubCommandsDescriptions(@NotNull Command cmd) { - final var subCommands = cmd.getSubCommands(); + final var subCommands = cmd.getCommands(); if (subCommands.isEmpty()) return null; final var buff = new StringBuilder(); diff --git a/src/main/java/lanat/helpRepresentation/HelpFormatter.java b/src/main/java/lanat/helpRepresentation/HelpFormatter.java index 91edd876..72ea7dd7 100644 --- a/src/main/java/lanat/helpRepresentation/HelpFormatter.java +++ b/src/main/java/lanat/helpRepresentation/HelpFormatter.java @@ -8,7 +8,6 @@ import lanat.utils.displayFormatter.FormatOption; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.*; @@ -16,43 +15,35 @@ * Manager for generating the help message of a command. It is possible to customize the layout of the help message by * overriding the {@link #initLayout()} method. *

    - * The layout is a list of {@link LayoutItem} objects, which are used to generate the help message. - * Each {@link LayoutItem} has a layout generator, which is a function that may take a {@link Command} as parameter and + * The layout is a list of {@link LayoutItem} objects, which are used to generate the help message. Each + * {@link LayoutItem} has a layout generator, which is a function that may take a {@link Command} as parameter and * returns a string. *

    *

    - * To generate the help message, use {@link #toString()}. + * To generate the help message, use {@link #generate(Command)} ()}. *

    + * * @see LayoutItem */ public class HelpFormatter { - Command parentCmd; private byte indentSize = 3; public static short lineWrapMax = 110; private @NotNull ArrayList<@NotNull LayoutItem> layout = new ArrayList<>(); public static boolean debugLayout = false; - public HelpFormatter(@Nullable Command parentCmd) { - this.parentCmd = parentCmd; - this.initLayout(); + static { Tag.initTags(); } - // the user can create a helpFormatter, though, the parentCmd should be applied later (otherwise stuff will fail) public HelpFormatter() { - this((Command)null); + this.initLayout(); } public HelpFormatter(@NotNull HelpFormatter other) { - this.parentCmd = other.parentCmd; this.indentSize = other.indentSize; this.layout.addAll(other.layout); } - public void setParentCmd(@NotNull Command parentCmd) { - this.parentCmd = parentCmd; - } - public void setIndentSize(int indentSize) { this.indentSize = (byte)Math.max(indentSize, 0); } @@ -70,16 +61,25 @@ public byte getIndentSize() { */ protected void initLayout() { this.setLayout( - LayoutItem.of(LayoutGenerators::title), - LayoutItem.of(LayoutGenerators::synopsis).indent(1).margin(1), - LayoutItem.of(LayoutGenerators::argumentDescriptions).title("Description:").indent(1), - LayoutItem.of(LayoutGenerators::subCommandsDescriptions).title("Sub-Commands:").indent(1).marginTop(1), - LayoutItem.of(LayoutGenerators::programLicense).marginTop(2) + LayoutItem.of(LayoutGenerators::titleAndDescription), + LayoutItem.of(LayoutGenerators::synopsis) + .indent(1) + .margin(1), + LayoutItem.of(LayoutGenerators::argumentDescriptions) + .title("Description:") + .indent(1), + LayoutItem.of(LayoutGenerators::subCommandsDescriptions) + .title("Sub-Commands:") + .indent(1) + .marginTop(1), + LayoutItem.of(LayoutGenerators::programLicense) + .marginTop(2) ); } /** * Moves a {@link LayoutItem} from one position to another. + * * @param from the index of the item to move * @param to the index to move the item to */ @@ -98,6 +98,7 @@ public final void moveLayoutItem(int from, int to) { /** * Adds one or more {@link LayoutItem} to the layout. + * * @param layoutItems the {@link LayoutItem} to add */ public final void addToLayout(@NotNull LayoutItem... layoutItems) { @@ -106,6 +107,7 @@ public final void addToLayout(@NotNull LayoutItem... layoutItems) { /** * Adds one or more {@link LayoutItem} to the layout at the specified position. + * * @param at the position to add the item/s at * @param layoutItems the item/s to add */ @@ -115,6 +117,7 @@ public final void addToLayout(int at, @NotNull LayoutItem... layoutItems) { /** * Sets the layout to the specified {@link LayoutItem} objects. + * * @param layoutItems the items to set the layout to */ public final void setLayout(@NotNull LayoutItem... layoutItems) { @@ -123,6 +126,7 @@ public final void setLayout(@NotNull LayoutItem... layoutItems) { /** * Removes one or more {@link LayoutItem} from the layout. + * * @param positions the positions of the items to remove */ public final void removeFromLayout(int... positions) { @@ -136,14 +140,14 @@ public final void removeFromLayout(int... positions) { /** * Generates the help message. + * * @return the help message */ - @Override - public @NotNull String toString() { + public @NotNull String generate(@NotNull Command cmd) { final var buffer = new StringBuilder(); for (int i = 0; i < this.layout.size(); i++) { - final var generatedContent = this.layout.get(i).generate(this); + final var generatedContent = this.layout.get(i).generate(this, cmd); if (generatedContent == null) continue; @@ -151,14 +155,14 @@ public final void removeFromLayout(int... positions) { if (HelpFormatter.debugLayout) buffer.append(new TextFormatter("LayoutItem " + i + ":\n") .addFormat(FormatOption.UNDERLINE) - .setColor(Color.GREEN) + .withForegroundColor(Color.GREEN) ); buffer.append(UtlString.wrap(generatedContent, lineWrapMax)).append('\n'); } - // UtlString.trim() is used because String.trim() also removes trailing \022 (escape character) - return UtlString.trim(buffer.toString()); + // strip() is used here because trim() also removes \022 (escape character) + return buffer.toString().strip(); } /** diff --git a/src/main/java/lanat/helpRepresentation/LayoutGenerators.java b/src/main/java/lanat/helpRepresentation/LayoutGenerators.java index d1de9bd7..bd3678df 100644 --- a/src/main/java/lanat/helpRepresentation/LayoutGenerators.java +++ b/src/main/java/lanat/helpRepresentation/LayoutGenerators.java @@ -9,10 +9,10 @@ import org.jetbrains.annotations.Nullable; import java.util.List; -import java.util.Objects; /** * This contains methods that may be used in {@link LayoutItem}s to generate the content of the help message. + * * @see LayoutItem */ public final class LayoutGenerators { @@ -20,34 +20,48 @@ private LayoutGenerators() {} /** * Shows the title of the command, followed by a description, if any. + * * @param cmd The command to generate the title for. * @return the generated title and description. */ - public static @NotNull String title(@NotNull Command cmd) { - return CommandRepr.getRepresentation(cmd) - + (cmd.getDescription() == null - ? "" - : ":\n\n" + HelpFormatter.indent(Objects.requireNonNull(DescriptionFormatter.parse(cmd)), cmd)); + public static @NotNull String titleAndDescription(@NotNull Command cmd) { + final var description = DescriptionFormatter.parse(cmd); + final var buff = new StringBuilder(CommandRepr.getRepresentation(cmd)); + + if (cmd instanceof ArgumentParser ap) { + final var version = ap.getVersion(); + if (version != null) { + buff.append(" (").append(version).append(')'); + } + } + + if (description != null) { + buff.append(":\n\n"); + buff.append(HelpFormatter.indent(description, cmd)); + } + + return buff.toString(); } /** * Shows the synopsis of the command, if any. *

    - * The synopsis is a list of all {@link Argument}s, {@link lanat.ArgumentGroup}s and - * Sub-{@link Command}s of the command. Each is shown with its own representation, as defined by the - * {@link ArgumentRepr}, {@link ArgumentGroupRepr} and {@link CommandRepr} classes. + * The synopsis is a list of all {@link Argument}s, {@link lanat.ArgumentGroup}s and Sub-{@link Command}s of the + * command. Each is shown with its own representation, as defined by the {@link ArgumentRepr}, + * {@link ArgumentGroupRepr} and {@link CommandRepr} classes. *

    *

    * First elements shown are the arguments, ordered by {@link Argument#sortByPriority(List)}, then the * {@link lanat.ArgumentGroup}s, which are shown recursively, and finally the sub-commands. *

    + * * @param cmd The command to generate the synopsis for. * @return the generated synopsis. */ public static @Nullable String synopsis(@NotNull Command cmd) { final var args = Argument.sortByPriority(cmd.getArguments()); - if (args.isEmpty() && cmd.getSubGroups().isEmpty()) return null; + if (args.isEmpty() && cmd.getGroups().isEmpty()) return null; final var buffer = new StringBuilder(); for (var arg : args) { @@ -58,18 +72,17 @@ private LayoutGenerators() {} buffer.append(ArgumentRepr.getRepresentation(arg)).append(' '); } - for (var group : cmd.getSubGroups()) { + for (var group : cmd.getGroups()) { buffer.append(ArgumentGroupRepr.getRepresentation(group)).append(' '); } - if (!cmd.getSubCommands().isEmpty()) + if (!cmd.getCommands().isEmpty()) buffer.append(' ').append(CommandRepr.getSubCommandsRepresentation(cmd)); return buffer.toString(); } /** - * * @param content Shows a heading with the given content, centered and surrounded by the given character. * @param lineChar The character to surround the content with. * @return the generated heading. @@ -80,19 +93,21 @@ private LayoutGenerators() {} /** * Shows a heading with the given content, centered and surrounded by dashes. + * * @param content The content of the heading. * @return the generated heading. */ public static @NotNull String heading(@NotNull String content) { - return UtlString.center(content, HelpFormatter.lineWrapMax); + return UtlString.center(content, HelpFormatter.lineWrapMax, '─'); } /** * Shows the descriptions of the {@link Argument}s and {@link lanat.ArgumentGroup}s of the command. *

    - * The descriptions are shown in the same order as the synopsis. If groups are present, they are shown - * recursively too, with their own descriptions and with the correct indentation level. + * The descriptions are shown in the same order as the synopsis. If groups are present, they are shown recursively + * too, with their own descriptions and with the correct indentation level. *

    + * * @param cmd The command to generate the descriptions for. * @return the generated descriptions. */ @@ -103,11 +118,11 @@ private LayoutGenerators() {} arg.getParentGroup() == null ).toList(); - if (arguments.isEmpty() && cmd.getSubGroups().isEmpty()) return null; + if (arguments.isEmpty() && cmd.getGroups().isEmpty()) return null; buff.append(ArgumentRepr.getDescriptions(arguments)); - for (var group : cmd.getSubGroups()) { + for (var group : cmd.getGroups()) { buff.append(ArgumentGroupRepr.getDescriptions(group)); } @@ -116,6 +131,7 @@ private LayoutGenerators() {} /** * Shows the descriptions of the sub-commands of the command. + * * @param cmd The command to generate the descriptions for. * @return the generated descriptions. */ @@ -129,6 +145,7 @@ private LayoutGenerators() {} * Note that this is a program-only property, so it will only be shown if the command is an instance of * {@link ArgumentParser}, that is, if it is the root command. *

    + * * @param cmd The command to generate the license for. * @return the generated license. * @see ArgumentParser#setLicense(String) diff --git a/src/main/java/lanat/helpRepresentation/LayoutItem.java b/src/main/java/lanat/helpRepresentation/LayoutItem.java index 3deb59c9..6d26f2db 100644 --- a/src/main/java/lanat/helpRepresentation/LayoutItem.java +++ b/src/main/java/lanat/helpRepresentation/LayoutItem.java @@ -9,9 +9,10 @@ import java.util.function.Supplier; /** - * Represents a layout item in the help message generated by {@link HelpFormatter}. - * This class is essentially just a builder with some helper commands for setting a {@link Function} that - * generates a {@link String} for a given {@link Command}. + * Represents a layout item in the help message generated by {@link HelpFormatter}. This class is essentially just a + * builder with some helper commands for setting a {@link Function} that generates a {@link String} for a given + * {@link Command}. + * * @see HelpFormatter */ public class LayoutItem { @@ -25,8 +26,9 @@ private LayoutItem(@NotNull Function<@NotNull Command, @Nullable String> layoutG } /** - * Creates a new {@link LayoutItem} with the given {@link Function} that generates a {@link String} - * for a given {@link Command}. + * Creates a new {@link LayoutItem} with the given {@link Function} that generates a {@link String} for a given + * {@link Command}. + * * @param layoutGenerator the function that generates the content of the layout item * @return the new LayoutItem */ @@ -36,6 +38,7 @@ public static LayoutItem of(@NotNull Function<@NotNull Command, @Nullable String /** * Creates a new {@link LayoutItem} with the given {@link Supplier} that generates a {@link String}. + * * @param layoutGenerator the supplier that generates the content of the layout item * @return the new LayoutItem */ @@ -45,6 +48,7 @@ public static LayoutItem of(@NotNull Supplier<@Nullable String> layoutGenerator) /** * Creates a new {@link LayoutItem} with the given {@link String} as content. + * * @param content the content of the layout item * @return the new LayoutItem */ @@ -54,8 +58,9 @@ public static LayoutItem of(@NotNull String content) { /** - * Sets the indent of the layout item. The indent is the number of indents that are added to the - * content of the layout item. The indent size is defined by {@link HelpFormatter#setIndentSize(int)}. + * Sets the indent of the layout item. The indent is the number of indents that are added to the content of the + * layout item. The indent size is defined by {@link HelpFormatter#setIndentSize(int)}. + * * @param indent the indent of the layout item */ public LayoutItem indent(int indent) { @@ -64,8 +69,9 @@ public LayoutItem indent(int indent) { } /** - * Sets the margin at the top of the layout item. The margin is the number of newlines that are added - * before the content of the layout item. + * Sets the margin at the top of the layout item. The margin is the number of newlines that are added before the + * content of the layout item. + * * @param marginTop the size of the margin at the top of the layout item */ public LayoutItem marginTop(int marginTop) { @@ -74,8 +80,9 @@ public LayoutItem marginTop(int marginTop) { } /** - * Sets the margin at the bottom of the layout item. The margin is the number of newlines that are added - * after the content of the layout item. + * Sets the margin at the bottom of the layout item. The margin is the number of newlines that are added after the + * content of the layout item. + * * @param marginBottom the size of the margin at the bottom of the layout item */ public LayoutItem marginBottom(int marginBottom) { @@ -86,6 +93,7 @@ public LayoutItem marginBottom(int marginBottom) { /** * Sets the margin at the top and bottom of the layout item. The margin is the number of newlines that are added * before and after the content of the layout item. + * * @param margin the size of the margin at the top and bottom of the layout item */ public LayoutItem margin(int margin) { @@ -94,14 +102,15 @@ public LayoutItem margin(int margin) { } /** - * Sets the title of the layout item. The title is added before the content of the layout - * item. The title is not indented. + * Sets the title of the layout item. The title is added before the content of the layout item. The title is not + * indented. *

    * It is shown as: *

     	 * <title<:
     	 *    <content>
     	 * 
    + * * @param title the title of the layout item */ public LayoutItem title(String title) { @@ -111,6 +120,7 @@ public LayoutItem title(String title) { /** * Returns the {@link Function} that generates the content of the layout item. + * * @return the layout generator */ public @NotNull Function<@NotNull Command, @Nullable String> getLayoutGenerator() { @@ -118,18 +128,19 @@ public LayoutItem title(String title) { } /** - * Generates the content of the layout item. - * The reason this method requires a {@link HelpFormatter} is because it provides the indent size and - * the parent command. + * Generates the content of the layout item. The reason this method requires a {@link HelpFormatter} is because it + * provides the indent size and the parent command. + * * @param helpFormatter the help formatter that is generating the help message * @return the content of the layout item */ - public @Nullable String generate(HelpFormatter helpFormatter) { - final var content = this.layoutGenerator.apply(helpFormatter.parentCmd); + public @Nullable String generate(@NotNull HelpFormatter helpFormatter, @NotNull Command cmd) { + final var content = this.layoutGenerator.apply(cmd); return content == null ? null : ( "\n".repeat(this.marginTop) + (this.title == null ? "" : this.title + "\n\n") - + UtlString.indent(UtlString.trim(content), this.indentCount * helpFormatter.getIndentSize()) + // strip() is used here because trim() also removes \022 (escape character) + + UtlString.indent(content.strip(), this.indentCount * helpFormatter.getIndentSize()) + "\n".repeat(this.marginBottom) ); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java b/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java index bf5d6372..5c784b2c 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java @@ -3,6 +3,7 @@ import lanat.CommandUser; import lanat.NamedWithDescription; import lanat.helpRepresentation.descriptions.exceptions.MalformedTagException; +import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -14,57 +15,57 @@ private DescriptionFormatter() {} /** * Parses the description of the given user and replaces all tags with the content generated by them. + * * @param user the user whose description is being parsed * @param desc the description to parse * @return the parsed description */ public static @NotNull String parse(@NotNull NamedWithDescription user, @NotNull String desc) { // if the description doesn't contain any tags, we can skip the parsing - if (!desc.contains(Character.toString(TAG_START)) && !desc.contains(Character.toString(TAG_END))) + if (!desc.contains(Character.toString(TAG_START))) return desc; final var chars = desc.toCharArray(); - final var out = new StringBuilder(); - final var current = new StringBuilder(); - boolean inTag = false; - int lastTagOpen = -1; + final var out = new StringBuilder(); // the output string + final var currentTag = new StringBuilder(); // the current tag being parsed + boolean inTag = false; // whether we are currently parsing a tag + int lastTagOpenIndex = -1; // the index of the last tag start character for (int i = 0; i < chars.length; i++) { final char chr = chars[i]; - if (chr == TAG_END && inTag) { - if (current.length() == 0) - throw new MalformedTagException("empty tag at index " + lastTagOpen); + if (chr == '\\') { + (inTag ? currentTag : out).append(chars[i == chars.length - 1 ? i : ++i]); + } else if (chr == TAG_END && inTag) { + if (currentTag.length() == 0) + throw new MalformedTagException("empty tag at index " + lastTagOpenIndex); - out.append(DescriptionFormatter.parseTag(current.toString(), user)); - current.setLength(0); + out.append(DescriptionFormatter.parseTag(currentTag.toString(), user)); + currentTag.setLength(0); inTag = false; } else if (chr == TAG_START && !inTag) { inTag = true; - lastTagOpen = i; - } else if (inTag) { - current.append(chr); - } else if (chr == '\\') { - out.append(chars[i == chars.length - 1 ? i : ++i]); + lastTagOpenIndex = i; } else { - out.append(chr); + (inTag ? currentTag : out).append(chr); } } if (inTag) { - throw new IllegalArgumentException("unclosed tag at index " + lastTagOpen); + throw new IllegalArgumentException("unclosed tag at index " + lastTagOpenIndex); } return out.toString(); } /** - * Parses the description of the given user and replaces all tags with the content generated by them. - * The description is taken from the given user by calling {@link NamedWithDescription#getDescription()}. + * Parses the description of the given user and replaces all tags with the content generated by them. The + * description is taken from the given user by calling {@link NamedWithDescription#getDescription()}. + * * @param element the user whose description is being parsed - * @return the parsed description, or null if the user has no description * @param the type of the user + * @return the parsed description, or null if the user has no description * @see #parse(NamedWithDescription, String) */ public static @@ -78,14 +79,15 @@ private DescriptionFormatter() {} /** * Parses the given tag and returns the content generated by its parser. + * * @param tagContents the contents of the tag, excluding the tag start and end characters * @param user the user whose description is being parsed * @return the content generated by the tag */ private static @NotNull String parseTag(@NotNull String tagContents, @NotNull NamedWithDescription user) { if (tagContents.contains("=")) { - final var split = tagContents.split("=", 2); - return Tag.parseTagValue(user, split[0].trim(), split[1].trim()); + final var split = UtlString.split(tagContents, '=', 2); + return Tag.parseTagValue(user, split[0], split[1]); } return Tag.parseTagValue(user, tagContents, null); diff --git a/src/main/java/lanat/helpRepresentation/descriptions/RouteParser.java b/src/main/java/lanat/helpRepresentation/descriptions/RouteParser.java index 8219f973..33ce217a 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/RouteParser.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/RouteParser.java @@ -15,9 +15,10 @@ /** * Parser for simple route syntax used in description tags (e.g. args.myArg1.type). *

    - * The route syntax is very simple. It is a dot-separated list of names indicating the path to the object to be returned. - * By default, the route initial target is the command the user belongs to. If the route starts with !, the - * user itself becomes the initial target. If the route is empty or null, the command the user belongs to is returned. + * The route syntax is very simple. It is a dot-separated list of names indicating the path to the object to be + * returned. By default, the route initial target is the command the user belongs to. If the route starts with + * !, the user itself becomes the initial target. If the route is empty or null, the command the user + * belongs to is returned. *

    *

    * These are the objects that can be accessed using the route syntax: @@ -59,7 +60,7 @@ * "!.type" * * - * */ + */ public class RouteParser { /** The current object being handled in the route */ private NamedWithDescription currentTarget; @@ -74,10 +75,12 @@ private RouteParser(@NotNull NamedWithDescription user, @Nullable String route) return; } - final String[] splitRoute = route.split("\\."); + final String[] splitRoute = UtlString.split(route, '.'); + // if route starts with !, the user itself is the target if (splitRoute[0].equals("!")) { this.currentTarget = user; + // slice the array to remove the first element (the !) this.route = Arrays.copyOfRange(splitRoute, 1, splitRoute.length); return; } @@ -87,12 +90,13 @@ private RouteParser(@NotNull NamedWithDescription user, @Nullable String route) } /** - * Parses a route and returns the object it points to. If the route is empty or null, the command the user belongs to - * is returned. + * Parses a route and returns the object it points to. If the route is empty or null, the command the user belongs + * to is returned. *

    - * The reason why the user is needed is because its likely that it will be needed to gather the Command it belongs to, - * and also if the route starts with !, the user itself becomes the initial target. + * The reason why the user is needed is because its likely that it will be needed to gather the Command it belongs + * to, and also if the route starts with !, the user itself becomes the initial target. *

    + * * @param user the user that is requesting to parse the route * @param route the route to parse * @return the object the route points to @@ -103,9 +107,10 @@ public static NamedWithDescription parse(@NotNull NamedWithDescription user, @Nu } /** - * Returns the command the object belongs to. If the object is a {@link Command}, it is returned. - * If it is a {@link CommandUser}, the command it belongs to is returned. Otherwise, - * an {@link InvalidRouteException} is thrown. + * Returns the command the object belongs to. If the object is a {@link Command}, it is returned. If it is a + * {@link CommandUser}, the command it belongs to is returned. Otherwise, an {@link InvalidRouteException} is + * thrown. + * * @param obj the object to get the command of * @return the command the object belongs to * @throws InvalidRouteException if the object is not a {@link Command} or a {@link CommandUser} @@ -115,14 +120,15 @@ public static Command getCommandOf(NamedWithDescription obj) { return cmd; } else if (obj instanceof CommandUser cmdUser) { return cmdUser.getParentCommand(); - } else { - throw new InvalidRouteException("Cannot get the Command " + obj.getName() + " belongs to"); } + + throw new InvalidRouteException("Cannot get the Command " + obj.getName() + " belongs to"); } /** - * Advances through the route and sets the current target to each element in the route. If the route is invalid, - * an {@link InvalidRouteException} is thrown. + * Advances through the route and sets the current target to each element in the route. If the route is invalid, an + * {@link InvalidRouteException} is thrown. + * * @return the object the route points to */ private NamedWithDescription parse() { @@ -132,9 +138,9 @@ private NamedWithDescription parse() { if (token.equals("args") && this.currentTarget instanceof ArgumentAdder argsContainer) this.setCurrentTarget(argsContainer.getArguments(), MultipleNamesAndDescription::hasName); else if (token.equals("groups") && this.currentTarget instanceof ArgumentGroupAdder groupsContainer) - this.setCurrentTarget(groupsContainer.getSubGroups(), (g, name) -> g.getName().equals(name)); + this.setCurrentTarget(groupsContainer.getGroups(), (g, name) -> g.getName().equals(name)); else if (token.equals("cmds") && this.currentTarget instanceof Command cmdsContainer) - this.setCurrentTarget(cmdsContainer.getSubCommands(), MultipleNamesAndDescription::hasName); + this.setCurrentTarget(cmdsContainer.getCommands(), MultipleNamesAndDescription::hasName); else if (token.equals("type") && this.currentTarget instanceof Argument arg) this.currentTarget = arg.argType; else @@ -146,9 +152,10 @@ else if (token.equals("type") && this.currentTarget instanceof Argument ar /** * Sets the current target to the first element in the list that matches the given predicate. + * * @param list the list to search in - * @param predicate the predicate to use to match the elements. The first parameter is the element, the second is the - * name to match against. + * @param predicate the predicate to use to match the elements. The first parameter is the element, the second is + * the name to match against. * @param the type of the elements in the list */ private diff --git a/src/main/java/lanat/helpRepresentation/descriptions/Tag.java b/src/main/java/lanat/helpRepresentation/descriptions/Tag.java index c9799eeb..212e6c03 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/Tag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/Tag.java @@ -6,24 +6,31 @@ import lanat.helpRepresentation.descriptions.tags.DescTag; import lanat.helpRepresentation.descriptions.tags.FormatTag; import lanat.helpRepresentation.descriptions.tags.LinkTag; +import lanat.utils.UtlReflection; +import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Hashtable; +import java.util.Map; +import java.util.regex.Pattern; + /** - * Class for handling parsing of the simple tags used in descriptions. (e.g. {@code }). - * Tags may receive no value, in which case the value received by the {@link #parse(NamedWithDescription, String)} - * method will be {@code null}. + * Class for handling parsing of the simple tags used in descriptions. (e.g. {@code }). Tags may + * receive no value, in which case the value received by the {@link #parse(NamedWithDescription, String)} method will be + * {@code null}. + * * @see #parse(NamedWithDescription, String) */ public abstract class Tag { - private static final Hashtable registeredTags = new Hashtable<>(); - private static boolean initializedTags; + private static final Hashtable> REGISTERED_TAGS = new Hashtable<>(); + private static final Pattern TAG_REGEX = Pattern.compile("[a-z][a-z-]+[a-z]", Pattern.CASE_INSENSITIVE); /** * This method will parse the tag value and return the parsed value. + * * @param user user that is parsing the tag * @param value value of the tag. May be {@code null} if the tag has no value specified. (e.g. {@code }) * @return parsed value of the tag @@ -34,37 +41,54 @@ public abstract class Tag { /** Initialize the tags. This method will register the default tags that are used in descriptions. */ public static void initTags() { - if (Tag.initializedTags) return; - - Tag.registerTag("link", new LinkTag()); - Tag.registerTag("desc", new DescTag()); - Tag.registerTag("color", new ColorTag()); - Tag.registerTag("format", new FormatTag()); + Tag.register("link", LinkTag.class); + Tag.register("desc", DescTag.class); + Tag.register("color", ColorTag.class); + Tag.register("format", FormatTag.class); + } - Tag.initializedTags = true; + public static String getTagNameFromTagClass(Class tagClass) { + return Tag.REGISTERED_TAGS.entrySet().stream() + .filter(entry -> entry.getValue() == tagClass) + .findFirst() + .map(Map.Entry::getKey) + .orElseThrow(() -> + new IllegalStateException("Tag class " + UtlString.surround(tagClass.getName()) + " is not registered") + ); } /** - * Register a tag instance to be used in descriptions. This class will be used to parse the tags encountered in the - * descriptions being parsed. - * @param name name of the tag (case-insensitive) + * Register a tag class to be used in descriptions. This class will be instantiated to parse the tags encountered in + * the descriptions being parsed. + * + * @param name name of the tag (case-insensitive). Must only contain lowercase letters and dashes. * @param tag tag object that will be used to parse the tag */ - public static void registerTag(@NotNull String name, @NotNull Tag tag) { - if (name.isEmpty()) throw new IllegalArgumentException("Tag name cannot be empty"); - Tag.registeredTags.put(name, tag); + public static void register(@NotNull String name, @NotNull Class tag) { + if (!Tag.TAG_REGEX.matcher(name).matches()) + throw new IllegalArgumentException("Tag name must only contain lowercase letters and dashes"); + Tag.REGISTERED_TAGS.put(name, tag); } /** * Parse a tag value. This method will parse the tag value using the tag registered with the given name. + * * @param user user that is parsing the tag * @param tagName name of the tag * @param value value of the tag * @return parsed value of the tag */ - static @NotNull String parseTagValue(@NotNull NamedWithDescription user, @NotNull String tagName, @Nullable String value) { - var tag = Tag.registeredTags.get(tagName.toLowerCase()); - if (tag == null) throw new UnknownTagException(tagName); - return tag.parse(user, value); + static @NotNull String parseTagValue( + @NotNull NamedWithDescription user, + @NotNull String tagName, + @Nullable String value + ) + { + final var tagClass = Tag.REGISTERED_TAGS.get(tagName.toLowerCase()); + + if (tagClass == null) + throw new UnknownTagException(tagName); + + return UtlReflection.instantiate(tagClass).parse(user, value); } } \ No newline at end of file diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java index 2e51e589..cd8064c3 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java @@ -1,13 +1,20 @@ package lanat.helpRepresentation.descriptions.exceptions; import lanat.exceptions.LanatException; +import lanat.helpRepresentation.descriptions.Tag; +import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** Thrown when a tag is malformed. */ public class MalformedTagException extends LanatException { - public MalformedTagException(@NotNull String tagName, @Nullable String reason) { - super("tag " + tagName + " is malformed" + (reason == null ? "" : ": " + reason)); + public MalformedTagException(@NotNull Class tagClass, @Nullable String reason) { + super( + "Tag " + + UtlString.surround(Tag.getTagNameFromTagClass(tagClass)) + + " is malformed" + + (reason == null ? "" : ": " + reason) + ); } public MalformedTagException(@NotNull String message) { diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java index 363ab80b..afdb1d00 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java @@ -11,7 +11,7 @@ public class NoDescriptionDefinedException extends LanatException { public NoDescriptionDefinedException(@NotNull NamedWithDescription user) { super( "No description defined for " - + UtlReflection.getSimpleName(user.getClass()) + " " + UtlString.surround(user.getName()) + + UtlReflection.getSimpleName(user.getClass()) + " " + UtlString.surround(user.getName()) ); } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java index fa884929..df96a573 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java @@ -11,8 +11,8 @@ import org.jetbrains.annotations.Nullable; /** - * Changes the color of the text. (e.g. {@code }). - * The available colors are the ones defined in {@link Color}. The color name is case-insensitive. + * Changes the color of the text. (e.g. {@code }). The available colors are the ones defined in + * {@link Color}. The color name is case-insensitive. *

    * The names that may be used are: *

      @@ -43,27 +43,27 @@ * If the color name is invalid, a {@link MalformedTagException} is thrown. * If no color is specified, the reset sequence is returned. (e.g. {@code }). *

      - * */ + */ public class ColorTag extends Tag { @Override protected @NotNull String parse(@NotNull NamedWithDescription user, @Nullable String value) { if (!TextFormatter.enableSequences) return ""; - if (value == null) return FormatOption.RESET_ALL.toString(); + if (value == null) return FormatOption.RESET_ALL.seq(); - if (!value.contains(":")) return getColor(value).toString(); + if (!value.contains(":")) return ColorTag.getColor(value).fg(); - final String[] split = value.split(":"); + final String[] split = UtlString.split(value, ':'); if (split.length != 2) throw new MalformedTagException( - "color", "invalid color format " + UtlString.surround(value) + ColorTag.class, "invalid color format " + UtlString.surround(value) + " (expected format: 'foreground:background')" ); - return getColor(split[0]).toString() + getColor(split[1]).toStringBackground(); + return ColorTag.getColor(split[0]).fg() + ColorTag.getColor(split[1]).bg(); } private static Color getColor(@NotNull String colorName) { - return switch (colorName.toLowerCase().trim().replaceAll("[_-]", " ")) { + return switch (colorName.toLowerCase().strip().replaceAll("[_-]", " ")) { case "black", "k" -> Color.BLACK; case "red", "r" -> Color.BRIGHT_RED; case "green", "g" -> Color.BRIGHT_GREEN; @@ -80,7 +80,7 @@ private static Color getColor(@NotNull String colorName) { case "dark magenta", "dm" -> Color.MAGENTA; case "dark cyan", "dc" -> Color.CYAN; case "dark white", "dw" -> Color.WHITE; - default -> throw new MalformedTagException("color", "unknown color name " + UtlString.surround(colorName)); + default -> throw new MalformedTagException(ColorTag.class, "unknown color name " + UtlString.surround(colorName)); }; } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/DescTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/DescTag.java index 5479148f..6ba62410 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/DescTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/DescTag.java @@ -11,8 +11,15 @@ /** * Gets the description of the target object specified by the route. + *

      + * Note that targeting the user itself is not allowed, as it would create an infinitely recursive description. Keep in + * mind that it is still possible to infinitely recurse by implicitly targeting the user, for example, by making an + * argument show the description of another argument, which in turn shows the description of the first argument too. + * This would eventually cause a {@link StackOverflowError}. + *

      + * * @see RouteParser - * */ + */ public class DescTag extends Tag { @Override protected @NotNull String parse(@NotNull NamedWithDescription user, @Nullable String value) { @@ -22,7 +29,7 @@ public class DescTag extends Tag { final var description = target.getDescription(); if (description == null) - throw new NoDescriptionDefinedException(user); + throw new NoDescriptionDefinedException(target); return DescriptionFormatter.parse(target, description); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java index 263c3da7..0674a667 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java @@ -10,18 +10,18 @@ import org.jetbrains.annotations.Nullable; /** - * Changes the format of the text. (e.g. {@code }). - * The available formats are the ones defined in {@link FormatOption}. The format name is case-insensitive. + * Changes the format of the text. (e.g. {@code }). The available formats are the ones defined in + * {@link FormatOption}. The format name is case-insensitive. *

      * The names that may be used are: *

        - *
      • reset / r
      • + *
      • reset
      • *
      • bold / b
      • *
      • italic / i
      • *
      • dim / d
      • *
      • underline / u
      • *
      • blink / bl
      • - *
      • reverse / re
      • + *
      • reverse / r
      • *
      • hidden / h
      • *
      • strike / s
      • *
      @@ -40,28 +40,29 @@ public class FormatTag extends Tag { @Override protected @NotNull String parse(@NotNull NamedWithDescription user, @Nullable String value) { if (!TextFormatter.enableSequences) return ""; - if (value == null) return FormatOption.RESET_ALL.toString(); + if (value == null) return FormatOption.RESET_ALL.seq(); final var buff = new StringBuilder(); - for (String opt : value.split(",")) - buff.append(opt.startsWith("!") ? getFormat(opt.substring(1)).toStringReset() : getFormat(opt)); + for (String opt : UtlString.split(value, ',')) + buff.append(opt.startsWith("!") ? getFormat(opt.substring(1)).reset() : getFormat(opt)); return buff.toString(); } private static FormatOption getFormat(@NotNull String formatName) { - return switch (formatName.toLowerCase().trim()) { - case "reset", "r" -> FormatOption.RESET_ALL; + return switch (formatName.toLowerCase().strip()) { + case "reset" -> FormatOption.RESET_ALL; case "bold", "b" -> FormatOption.BOLD; case "italic", "i" -> FormatOption.ITALIC; case "dim", "d" -> FormatOption.DIM; case "underline", "u" -> FormatOption.UNDERLINE; case "blink", "bl" -> FormatOption.BLINK; - case "reverse", "re" -> FormatOption.REVERSE; + case "reverse", "r" -> FormatOption.REVERSE; case "hidden", "h" -> FormatOption.HIDDEN; case "strike", "s" -> FormatOption.STRIKE_THROUGH; - default -> throw new MalformedTagException("format", "unknown format name " + UtlString.surround(formatName)); + default -> + throw new MalformedTagException(FormatTag.class, "unknown format name " + UtlString.surround(formatName)); }; } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/LinkTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/LinkTag.java index 9b924f10..55a650c4 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/LinkTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/LinkTag.java @@ -15,8 +15,9 @@ /** * Gets the representation of the target object specified by the route. + * * @see RouteParser - * */ + */ public class LinkTag extends Tag { @Override protected @NotNull String parse(@NotNull NamedWithDescription user, @Nullable String value) { diff --git a/src/main/java/lanat/parsing/Parser.java b/src/main/java/lanat/parsing/Parser.java index 0ffce3a5..a492baa7 100644 --- a/src/main/java/lanat/parsing/Parser.java +++ b/src/main/java/lanat/parsing/Parser.java @@ -82,37 +82,41 @@ public void setTokens(@NotNull List<@NotNull Token> tokens) { } public void parseTokens() { - if (this.tokens == null) - throw new IllegalStateException("Tokens have not been set yet."); + assert this.tokens != null : "Tokens have not been set yet"; + assert !this.hasFinished : "This parser has already finished parsing."; - if (this.hasFinished) - throw new IllegalStateException("This parser has already finished parsing."); - - - short argumentNameCount = 0; - boolean foundNonPositionalArg = false; - Argument lastPosArgument; // this will never be null when being used + // number of positional arguments that have been parsed. + // if this becomes -1, then we know that we are no longer parsing positional arguments + short positionalArgCount = 0; + Argument lastPositionalArgument; // this will never be null when being used for (this.currentTokenIndex = 0; this.currentTokenIndex < this.tokens.size(); ) { final Token currentToken = this.tokens.get(this.currentTokenIndex); if (currentToken.type() == TokenType.ARGUMENT_NAME) { + // encountered an argument name. first skip the token of the name. this.currentTokenIndex++; + // find the argument that matches that name and let it parse the values this.runForArgument(currentToken.contents(), this::executeArgParse); - foundNonPositionalArg = true; + // we encountered an argument name, so we know that we are no longer parsing positional arguments + positionalArgCount = -1; } else if (currentToken.type() == TokenType.ARGUMENT_NAME_LIST) { + // in a name list, skip the first character because it is the indicator that it is a name list this.parseArgNameList(currentToken.contents().substring(1)); - foundNonPositionalArg = true; + positionalArgCount = -1; } else if ( (currentToken.type() == TokenType.ARGUMENT_VALUE || currentToken.type() == TokenType.ARGUMENT_VALUE_TUPLE_START) - && !foundNonPositionalArg - && (lastPosArgument = this.getArgumentByPositionalIndex(argumentNameCount)) != null - ) - { // this is most likely a positional argument - this.executeArgParse(lastPosArgument); - argumentNameCount++; + && positionalArgCount != -1 + && (lastPositionalArgument = this.getArgumentByPositionalIndex(positionalArgCount)) != null + ) { + // if we are here we encountered an argument value with no prior argument name or name list, + // so this must be a positional argument + this.executeArgParse(lastPositionalArgument); + positionalArgCount++; } else { + // addError depends on the currentTokenIndex, so we need to increment it before calling it this.currentTokenIndex++; + if (currentToken.type() != TokenType.FORWARD_VALUE) this.addError(ParseError.ParseErrorType.UNMATCHED_TOKEN, null, 0); } @@ -121,7 +125,7 @@ public void parseTokens() { this.hasFinished = true; // now parse the Sub-Commands - this.getSubCommands().stream() + this.getCommands().stream() .filter(sb -> sb.getTokenizer().isFinishedTokenizing()) // only get the commands that were actually tokenized .forEach(sb -> sb.getParser().parseTokens()); // now parse them } @@ -134,10 +138,10 @@ public void parseTokens() { *

      */ private void executeArgParse(@NotNull Argument arg) { - final Range argumentValuesRange = arg.argType.getRequiredArgValueCount(); + final Range argNumValuesRange = arg.argType.getRequiredArgValueCount(); // just skip the whole thing if it doesn't need any values - if (argumentValuesRange.isZero()) { + if (argNumValuesRange.isZero()) { arg.parseValues(this.currentTokenIndex); return; } @@ -147,45 +151,48 @@ private void executeArgParse(@NotNull Argument arg) { && this.tokens.get(this.currentTokenIndex).type() == TokenType.ARGUMENT_VALUE_TUPLE_START ); - final int ifTupleOffset = isInTuple ? 1 : 0; - int skipCount = ifTupleOffset; + final byte ifTupleOffset = (byte)(isInTuple ? 1 : 0); - final ArrayList tempArgs = new ArrayList<>(); + final ArrayList values = new ArrayList<>(); + short numValues = 0; // add more values until we get to the max of the type, or we encounter another argument specifier for ( - int i = this.currentTokenIndex + ifTupleOffset; - i < this.tokens.size(); - i++, skipCount++ + int tokenIndex = this.currentTokenIndex + ifTupleOffset; + tokenIndex < this.tokens.size(); + numValues++, tokenIndex++ ) { - final Token currentToken = this.tokens.get(i); + final Token currentToken = this.tokens.get(tokenIndex); if (!isInTuple && ( - currentToken.type().isArgumentSpecifier() || i - this.currentTokenIndex >= argumentValuesRange.max() - ) - || currentToken.type().isTuple() + currentToken.type().isArgumentSpecifier() || numValues >= argNumValuesRange.max() + ) + || currentToken.type().isTuple() ) break; - tempArgs.add(currentToken); + values.add(currentToken); } - final int tempArgsSize = tempArgs.size(); - final int newCurrentTokenIndex = skipCount + ifTupleOffset; + // add 2 if we are in a tuple, because we need to skip the start and end tuple tokens + final int skipIndexCount = numValues + ifTupleOffset*2; - if (tempArgsSize > argumentValuesRange.max() || tempArgsSize < argumentValuesRange.min()) { - this.addError(ParseError.ParseErrorType.ARG_INCORRECT_VALUE_NUMBER, arg, tempArgsSize + ifTupleOffset); - this.currentTokenIndex += newCurrentTokenIndex; + if (numValues > argNumValuesRange.max() || numValues < argNumValuesRange.min()) { + this.addError(ParseError.ParseErrorType.ARG_INCORRECT_VALUE_NUMBER, arg, numValues + ifTupleOffset); + this.currentTokenIndex += skipIndexCount; return; } // pass the arg values to the argument sub parser - arg.parseValues(tempArgs.stream().map(Token::contents).toArray(String[]::new), (short)(this.currentTokenIndex + ifTupleOffset)); + arg.parseValues( + (short)(this.currentTokenIndex + ifTupleOffset), + values.stream().map(Token::contents).toArray(String[]::new) + ); - this.currentTokenIndex += newCurrentTokenIndex; + this.currentTokenIndex += skipIndexCount; } /** * Parses the given string as an argument value for the given argument. *

      - * If the value passed in is present (not empty or null), the argument should only require 0 or 1 values. + * If the value passed in is present (not empty or {@code null}), the argument should only require 0 or 1 values. *

      */ private void executeArgParse(@NotNull Argument arg, @Nullable String value) { @@ -208,7 +215,7 @@ private void executeArgParse(@NotNull Argument arg, @Nullable String value } // pass the arg values to the argument subParser - arg.parseValues(new String[] { value }, this.currentTokenIndex); + arg.parseValues(this.currentTokenIndex, value); } /** @@ -253,7 +260,11 @@ private void parseArgNameList(@NotNull String args) { return null; } - /** Returns a hashmap of Arguments and their corresponding parsed values. */ + /** + * Returns a hashmap of Arguments and their corresponding parsed values. + * This function invokes the {@link Argument#finishParsing()} method on each argument the first time it is called. + * After that, it will return the same hashmap. + * */ public @NotNull HashMap<@NotNull Argument, @Nullable Object> getParsedArgumentsHashMap() { if (this.parsedArguments == null) { this.parsedArguments = new HashMap<>() {{ diff --git a/src/main/java/lanat/parsing/ParsingStateBase.java b/src/main/java/lanat/parsing/ParsingStateBase.java index 1139f4b7..12e5cd13 100644 --- a/src/main/java/lanat/parsing/ParsingStateBase.java +++ b/src/main/java/lanat/parsing/ParsingStateBase.java @@ -38,7 +38,7 @@ protected boolean runForArgument(@NotNull String argName, @NotNull Consumer<@Not /** * Executes a callback for the argument found by the name specified. * - * @return true if an argument was found + * @return {@code true} if an argument was found */ /* This method right here looks like it could be replaced by just changing it to * return this.runForArgument(String.valueOf(argName), f); @@ -60,8 +60,8 @@ protected boolean runForArgument(char argName, @NotNull Consumer<@NotNull Argume return this.command.getArguments(); } - protected @NotNull List<@NotNull Command> getSubCommands() { - return this.command.getSubCommands(); + protected @NotNull List<@NotNull Command> getCommands() { + return this.command.getCommands(); } public boolean hasFinished() { diff --git a/src/main/java/lanat/parsing/Token.java b/src/main/java/lanat/parsing/Token.java index acddce97..3534032c 100644 --- a/src/main/java/lanat/parsing/Token.java +++ b/src/main/java/lanat/parsing/Token.java @@ -5,6 +5,13 @@ import org.jetbrains.annotations.NotNull; public record Token(@NotNull TokenType type, @NotNull String contents) { + /** + * Returns a {@link TextFormatter} instance that can be used to display the token. + *

      + * This instance uses the {@link TokenType#color} property to color the token. + *

      + * @return A {@link TextFormatter} instance that can be used to display the token. + */ public @NotNull TextFormatter getFormatter() { var contents = this.contents(); if (contents.contains(" ") && this.type == TokenType.ARGUMENT_VALUE) { diff --git a/src/main/java/lanat/parsing/Tokenizer.java b/src/main/java/lanat/parsing/Tokenizer.java index a7dcb475..0e4726e8 100644 --- a/src/main/java/lanat/parsing/Tokenizer.java +++ b/src/main/java/lanat/parsing/Tokenizer.java @@ -30,6 +30,9 @@ public class Tokenizer extends ParsingStateBase { /** The input string that is being tokenized, split into characters */ private char[] inputChars; + final char tupleOpenChar = this.command.getTupleChars().open; + final char tupleCloseChar = this.command.getTupleChars().close; + public Tokenizer(@NotNull Command command) { super(command); @@ -51,19 +54,12 @@ private void setInputString(@NotNull String inputString) { * {@link Tokenizer#getFinalTokens()} */ public void tokenize(@NotNull String input) { - if (this.hasFinished) { - throw new IllegalStateException("Tokenizer has already finished tokenizing"); - } + assert !this.hasFinished : "Tokenizer has already finished tokenizing"; this.setInputString(input); - final var values = new Object() { - char currentStringChar = 0; - TokenizeError.TokenizeErrorType errorType = null; - final char tupleOpenChar = Tokenizer.this.command.getTupleChars().open; - final char tupleCloseChar = Tokenizer.this.command.getTupleChars().close; - }; - + char currentStringChar = 0; // the character that opened the string + TokenizeError.TokenizeErrorType errorType = null; for ( this.currentCharIndex = 0; @@ -80,7 +76,7 @@ public void tokenize(@NotNull String input) { } else if (cChar == '"' || cChar == '\'') { // if we are already in an open string, push the current value and close the string. Make sure // that the current char is the same as the one that opened the string - if (this.stringOpen && values.currentStringChar == cChar) { + if (this.stringOpen && currentStringChar == cChar) { this.addToken(TokenType.ARGUMENT_VALUE, this.currentValue.toString()); this.currentValue.setLength(0); this.stringOpen = false; @@ -92,7 +88,7 @@ public void tokenize(@NotNull String input) { // the string is not open, so open it and set the current string char to the current char } else { this.stringOpen = true; - values.currentStringChar = cChar; + currentStringChar = cChar; } // append characters to the current value as long as we are in a string @@ -100,24 +96,24 @@ public void tokenize(@NotNull String input) { this.currentValue.append(cChar); // reached a possible tuple start character - } else if (cChar == values.tupleOpenChar) { + } else if (cChar == this.tupleOpenChar) { // if we are already in a tuple, set error and stop tokenizing if (this.tupleOpen) { - values.errorType = TokenizeError.TokenizeErrorType.TUPLE_ALREADY_OPEN; + errorType = TokenizeError.TokenizeErrorType.TUPLE_ALREADY_OPEN; break; } else if (!this.currentValue.isEmpty()) { // if there was something before the tuple, tokenize it this.tokenizeCurrentValue(); } // push the tuple token and set the state to tuple open - this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_START, values.tupleOpenChar); + this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_START, this.tupleOpenChar); this.tupleOpen = true; // reached a possible tuple end character - } else if (cChar == values.tupleCloseChar) { + } else if (cChar == this.tupleCloseChar) { // if we are not in a tuple, set error and stop tokenizing if (!this.tupleOpen) { - values.errorType = TokenizeError.TokenizeErrorType.UNEXPECTED_TUPLE_CLOSE; + errorType = TokenizeError.TokenizeErrorType.UNEXPECTED_TUPLE_CLOSE; break; } @@ -127,7 +123,7 @@ public void tokenize(@NotNull String input) { } // push the tuple token and set the state to tuple closed - this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_END, values.tupleCloseChar); + this.addToken(TokenType.ARGUMENT_VALUE_TUPLE_END, this.tupleCloseChar); this.currentValue.setLength(0); this.tupleOpen = false; @@ -156,11 +152,11 @@ public void tokenize(@NotNull String input) { } } - if (values.errorType == null) + if (errorType == null) if (this.tupleOpen) { - values.errorType = TokenizeError.TokenizeErrorType.TUPLE_NOT_CLOSED; + errorType = TokenizeError.TokenizeErrorType.TUPLE_NOT_CLOSED; } else if (this.stringOpen) { - values.errorType = TokenizeError.TokenizeErrorType.STRING_NOT_CLOSED; + errorType = TokenizeError.TokenizeErrorType.STRING_NOT_CLOSED; } // we left something in the current value, tokenize it @@ -168,8 +164,8 @@ public void tokenize(@NotNull String input) { this.tokenizeCurrentValue(); } - if (values.errorType != null) { - this.addError(values.errorType, this.finalTokens.size()); + if (errorType != null) { + this.addError(errorType, this.finalTokens.size()); } this.hasFinished = true; @@ -228,10 +224,10 @@ private void tokenizeCurrentValue() { } /** - * Returns true if the given string can be an argument name list, eg: "-fbq". + * Returns {@code true} if the given string can be an argument name list, eg: "-fbq". *

      - * This returns true if at least the first character is a valid argument prefix and at least one of the next - * characters is a valid argument name. + * This returns {@code true} if at least the first character is a valid argument prefix and at least one of the + * next characters is a valid argument name. *

      * For a prefix to be valid, it must be a character used as a prefix on the next argument/s specified. *

      @@ -251,9 +247,9 @@ private boolean isArgNameList(@NotNull String str) { } /** - * Returns true if the given string can be an argument name, eg: "--help". + * Returns {@code true} if the given string can be an argument name, eg: "--help". *

      - * This returns true if the given string is a valid argument name with a double prefix. + * This returns {@code true} if the given string is a valid argument name with a double prefix. *

      */ private boolean isArgName(@NotNull String str) { @@ -262,23 +258,23 @@ private boolean isArgName(@NotNull String str) { } /** - * Returns true whether the given string is an argument name {@link Tokenizer#isArgName(String)} or an argument name - * list {@link Tokenizer#isArgNameList(String)}. + * Returns {@code true} whether the given string is an argument name {@link Tokenizer#isArgName(String)} or an + * argument name list {@link Tokenizer#isArgNameList(String)}. */ private boolean isArgumentSpecifier(@NotNull String str) { return this.isArgName(str) || this.isArgNameList(str); } - /** Returns true if the given string is a Sub-Command name */ + /** Returns {@code true} if the given string is a Sub-Command name */ private boolean isSubCommand(@NotNull String str) { - return this.getSubCommands().stream().anyMatch(c -> c.hasName(str)); + return this.getCommands().stream().anyMatch(c -> c.hasName(str)); } /** - * Returns true if the character of {@link Tokenizer#inputChars} at a relative index from + * Returns {@code true} if the character of {@link Tokenizer#inputChars} at a relative index from * {@link Tokenizer#currentCharIndex} is equal to the specified character. *

      - * If the index is out of bounds, returns false. + * If the index is out of bounds, returns {@code false}. *

      */ private boolean isCharAtRelativeIndex(int index, char character) { @@ -289,7 +285,7 @@ private boolean isCharAtRelativeIndex(int index, char character) { /** Returns a command from the Sub-Commands of {@link Tokenizer#command} that matches the given name */ private Command getSubCommandByName(@NotNull String name) { - var x = this.getSubCommands().stream().filter(sc -> sc.hasName(name)).toList(); + var x = this.getCommands().stream().filter(sc -> sc.hasName(name)).toList(); return x.isEmpty() ? null : x.get(0); } @@ -299,20 +295,20 @@ private Command getSubCommandByName(@NotNull String name) { * Note that a Command only has a single tokenized Sub-Command, so this will have one Command per nesting level. *

      */ - public @NotNull List<@NotNull Command> getTokenizedSubCommands() { + public @NotNull List<@NotNull Command> getTokenizedCommands() { final List x = new ArrayList<>(); final Command subCmd; x.add(this.command); if ((subCmd = this.getTokenizedSubCommand()) != null) { - x.addAll(subCmd.getTokenizer().getTokenizedSubCommands()); + x.addAll(subCmd.getTokenizer().getTokenizedCommands()); } return x; } /** Returns the tokenized Sub-Command of {@link Tokenizer#command}. */ public @Nullable Command getTokenizedSubCommand() { - return this.getSubCommands().stream() + return this.getCommands().stream() .filter(sb -> sb.getTokenizer().hasFinished) .findFirst() .orElse(null); @@ -320,9 +316,7 @@ private Command getSubCommandByName(@NotNull String name) { /** Returns the list of all tokens that have been tokenized. */ public @NotNull List<@NotNull Token> getFinalTokens() { - if (!this.hasFinished) { - throw new IllegalStateException("Cannot get final tokens before tokenizing is finished!"); - } + assert this.hasFinished : "Cannot get final tokens before tokenizing has finished"; return this.finalTokens; } diff --git a/src/main/java/lanat/parsing/errors/CustomError.java b/src/main/java/lanat/parsing/errors/CustomError.java index 0c448267..61dfd492 100644 --- a/src/main/java/lanat/parsing/errors/CustomError.java +++ b/src/main/java/lanat/parsing/errors/CustomError.java @@ -10,12 +10,13 @@ public class CustomError extends ParseStateErrorBase tokens; + /** The root command. */ private final @NotNull Command rootCmd; - private int absoluteCmdTokenIndex = 0; + /** The index of the last command encountered in the token list. */ + private int absoluteCmdTokenIndex = -1; + /** + * Creates a new error handler for the specified command. + * @param rootCommand The root command. + */ public ErrorHandler(@NotNull Command rootCommand) { this.rootCmd = rootCommand; this.tokens = Collections.unmodifiableList(rootCommand.getFullTokenList()); } /** - * Handles all errors and returns a list of them. + * Handles all errors and returns a list of error messages generated. */ - public @NotNull List<@NotNull String> handleErrorsGetMessages() { - final List commands = this.rootCmd.getTokenizer().getTokenizedSubCommands(); + public @NotNull List<@NotNull String> handleErrors() { + final List commands = this.rootCmd.getTokenizer().getTokenizedCommands(); final ArrayList errors = new ArrayList<>(); for (int i = 0; i < commands.size(); i++) { final Command cmd = commands.get(i); this.absoluteCmdTokenIndex = this.getCommandTokenIndexByNestingLevel(i); + // gather all errors new ArrayList>() {{ this.addAll(cmd.getErrorsUnderDisplayLevel()); this.addAll(cmd.getTokenizer().getErrorsUnderDisplayLevel()); this.addAll(cmd.getParser().getCustomErrors()); this.addAll(ParseError.filter(cmd.getParser().getErrorsUnderDisplayLevel())); }}.stream() - .sorted(Comparator.comparingInt(x -> x.tokenIndex)) - .forEach(e -> errors.add(e.handle(this))); + .sorted(Comparator.comparingInt(x -> x.tokenIndex)) // sort them by their token index... + .forEach(e -> errors.add(e.handle(this))); // ...and handle them } return Collections.unmodifiableList(errors); @@ -59,16 +69,16 @@ public ErrorHandler(@NotNull Command rootCommand) { * token list like this:
      *
      {@code
       	 * {
      -	 *   SUB_COMMAND,
      +	 *   COMMAND,
       	 *   ARGUMENT_NAME,
       	 *   ARGUMENT_VALUE,
      -	 *   SUB_COMMAND, // <- here
      +	 *   COMMAND, // <- here
       	 *   ARGUMENT_NAME_LIST,
      -	 *   SUB_COMMAND,
      +	 *   COMMAND,
       	 *   ARGUMENT_NAME
       	 * }}
      - * The nesting level of the second Sub-Command is 1 (starting at 0), and its index in the token list - * is 3. + * The nesting level of the second Sub-Command is 1 (starting at 0), and its index in the token + * list is 3. * * @return -1 if the command is not found. */ @@ -87,14 +97,18 @@ private int getCommandTokenIndexByNestingLevel(int level) { return -1; } - public int getErrorCode() { - return this.rootCmd.getErrorCode(); - } - - public @NotNull Command getRootCmd() { + /** + * Returns the root command that this error handler is handling errors for. + * @return The root command that this error handler is handling errors for. + */ + public @NotNull Command getRootCommand() { return this.rootCmd; } + /** + * Returns the index of the current command in the token list. + * @return The index of the current command in the token list. + */ public int getAbsoluteCmdTokenIndex() { return this.absoluteCmdTokenIndex; } diff --git a/src/main/java/lanat/parsing/errors/ParseError.java b/src/main/java/lanat/parsing/errors/ParseError.java index ebc901d2..3222031a 100644 --- a/src/main/java/lanat/parsing/errors/ParseError.java +++ b/src/main/java/lanat/parsing/errors/ParseError.java @@ -75,7 +75,7 @@ protected void handleIncorrectValueNumber() { assert this.argument != null; this.fmt() - .setContents("Incorrect number of values for argument '%s'.%nExpected %s, but got %d." + .setContent("Incorrect number of values for argument '%s'.%nExpected %s, but got %d." .formatted( this.argument.getName(), this.argument.argType.getRequiredArgValueCount().getMessage("value"), Math.max(this.valueCount - 1, 0) // this is done because if there are tuples, the end token is counted as a value (maybe a bit hacky?) @@ -89,7 +89,7 @@ protected void handleIncorrectUsagesCount() { assert this.argument != null; this.fmt() - .setContents("Argument '%s' was used an incorrect amount of times.%nExpected %s, but was used %s." + .setContent("Argument '%s' was used an incorrect amount of times.%nExpected %s, but was used %s." .formatted( this.argument.getName(), this.argument.argType.getRequiredUsageCount().getMessage("usage"), UtlString.plural("time", this.argument.getUsageCount()) @@ -104,7 +104,7 @@ protected void handleObligatoryArgumentNotUsed() { final var argCmd = this.argument.getParentCommand(); this.fmt() - .setContents( + .setContent( argCmd instanceof ArgumentParser ? "Obligatory argument '%s' not used.".formatted(this.argument.getName()) : "Obligatory argument '%s' for command '%s' not used.".formatted(this.argument.getName(), argCmd.getName()) @@ -115,7 +115,7 @@ protected void handleObligatoryArgumentNotUsed() { @Handler("UNMATCHED_TOKEN") protected void handleUnmatchedToken() { this.fmt() - .setContents("Token '%s' does not correspond with a valid argument, value, or command." + .setContent("Token '%s' does not correspond with a valid argument, value, or command." .formatted(this.getCurrentToken().contents()) ) .displayTokens(this.tokenIndex, this.valueCount, false); @@ -124,8 +124,8 @@ protected void handleUnmatchedToken() { @Handler("MULTIPLE_ARGS_IN_EXCLUSIVE_GROUP_USED") protected void handleMultipleArgsInExclusiveGroupUsed() { this.fmt() - .setContents("Multiple arguments in exclusive group '%s' used." - .formatted(this.argumentGroup.name) + .setContent("Multiple arguments in exclusive group '%s' used." + .formatted(this.argumentGroup.getName()) ) .displayTokens(this.tokenIndex, this.valueCount, false); } diff --git a/src/main/java/lanat/parsing/errors/ParseStateErrorBase.java b/src/main/java/lanat/parsing/errors/ParseStateErrorBase.java index 0ce75820..8d354027 100644 --- a/src/main/java/lanat/parsing/errors/ParseStateErrorBase.java +++ b/src/main/java/lanat/parsing/errors/ParseStateErrorBase.java @@ -1,23 +1,20 @@ package lanat.parsing.errors; -import fade.mirror.MClass; -import fade.mirror.MMethod; -import fade.mirror.exception.MirrorException; -import fade.mirror.filter.Filter; import lanat.ErrorFormatter; import lanat.ErrorLevel; import lanat.parsing.Token; import lanat.utils.ErrorLevelProvider; +import lanat.utils.UtlReflection; import org.jetbrains.annotations.NotNull; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.List; -import java.util.stream.Collectors; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Optional; -import static fade.mirror.Mirror.mirror; /** * Provides a {@link ParseStateErrorBase#handle(ErrorHandler)} method that when called, automatically invokes the @@ -64,63 +61,57 @@ * @param An enum with the possible error types to handle. */ abstract class ParseStateErrorBase & ErrorLevelProvider> implements ErrorLevelProvider { + /** The type of the error. */ public final @NotNull T errorType; - private final List> methods; + + /** The index of the token that caused the error. */ public int tokenIndex; + + /** The error handler that handles the error. */ private ErrorHandler errorHandler; + + /** The error formatter that formats the error message. */ private ErrorFormatter formatter; + + /** + * Creates a new error handler for the specified type of error. + * @param errorType The type of the error. + * @param tokenIndex The index of the token that caused the error. + */ public ParseStateErrorBase(@NotNull T errorType, int tokenIndex) { this.errorType = errorType; this.tokenIndex = tokenIndex; - - // check if there are methods defined for all error types - this.methods = this.getAnnotatedMethods(); - - for (final var handlerName : this.errorType.getClass().getEnumConstants()) { - final var handlerNameStr = handlerName.name(); - - // throw an exception if there is no method defined for the error type - if (this.methods.stream().noneMatch(m -> this.isHandlerMethod(m, handlerNameStr))) - throw new IllegalStateException("No method defined for error type " + handlerNameStr); - } - } - - private @NotNull List<@NotNull MMethod> getAnnotatedMethods() { - return mirror(this.getClass()) - .getSuperclassUntil(MClass::hasMethods, MClass.IncludeSelf.Yes) - .>>map(objectMClass -> objectMClass.getMethods(Filter.forMethods().withAnnotation(Handler.class)) - .collect(Collectors.toList())) - .orElseGet(List::of); - } - - private boolean isHandlerMethod(@NotNull MMethod method, @NotNull String handlerName) { - return method.getAnnotationOfType(Handler.class) - .map(handler -> handler.value().equals(handlerName)) - .orElse(false); } - private boolean isHandlerMethod(@NotNull MMethod method) { - return this.isHandlerMethod(method, this.errorType.name()); + /** + * Returns the method that should be called to handle the error. + * @throws RuntimeException If no handler method is defined for the error type. + * @return The handler method. + */ + private @NotNull Method getHandlerMethod() { + return UtlReflection.getMethods(this.getClass()) + .filter(m -> Optional.ofNullable(m.getAnnotation(Handler.class)) + .map(a -> a.value().equals(this.errorType.name())) + .orElse(false) + ) + .findFirst() + .orElseThrow(() -> new RuntimeException("No handler method defined for error type " + this.errorType.name())); } + /** + * Handles the error by calling the appropriate handler method. + * @param handler The error handler. + * @return The error message. + */ public final @NotNull String handle(@NotNull ErrorHandler handler) { this.errorHandler = handler; this.formatter = new ErrorFormatter(handler, this.errorType.getErrorLevel()); - // invoke the method if it is defined - for (final var method : this.methods) { - if (!this.isHandlerMethod(method)) continue; - - try { - method.bindToObject(this) - .requireAccessible() - .invoke(); - - } catch (MirrorException e) { - throw new RuntimeException(e); - } - return this.formatter.toString(); + try { + this.getHandlerMethod().invoke(this); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); } return this.formatter.toString(); @@ -131,6 +122,9 @@ private boolean isHandlerMethod(@NotNull MMethod method) { return this.errorType.getErrorLevel(); } + /** + * Returns the token at the index of this error. + */ protected @NotNull Token getCurrentToken() { return this.errorHandler.getRelativeToken(this.tokenIndex); } @@ -142,6 +136,7 @@ private boolean isHandlerMethod(@NotNull MMethod method) { return this.formatter; } + /** Annotation that defines a handler method for a specific error type. */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public @interface Handler { diff --git a/src/main/java/lanat/parsing/errors/TokenizeError.java b/src/main/java/lanat/parsing/errors/TokenizeError.java index 4aebc1e2..9122be83 100644 --- a/src/main/java/lanat/parsing/errors/TokenizeError.java +++ b/src/main/java/lanat/parsing/errors/TokenizeError.java @@ -25,28 +25,28 @@ public TokenizeError(@NotNull TokenizeErrorType type, int index) { @Handler("TUPLE_ALREADY_OPEN") protected void handleTupleAlreadyOpen() { this.fmt() - .setContents("Tuple already open.") + .setContent("Tuple already open.") .displayTokens(this.tokenIndex + 1); } @Handler("TUPLE_NOT_CLOSED") protected void handleTupleNotClosed() { this.fmt() - .setContents("Tuple not closed.") + .setContent("Tuple not closed.") .displayTokens(this.tokenIndex + 1); } @Handler("UNEXPECTED_TUPLE_CLOSE") protected void handleUnexpectedTupleClose() { this.fmt() - .setContents("Unexpected tuple close.") + .setContent("Unexpected tuple close.") .displayTokens(this.tokenIndex + 1); } @Handler("STRING_NOT_CLOSED") protected void handleStringNotClosed() { this.fmt() - .setContents("String not closed.") + .setContent("String not closed.") .displayTokens(this.tokenIndex + 1); } } diff --git a/src/main/java/lanat/utils/ErrorCallbacks.java b/src/main/java/lanat/utils/ErrorCallbacks.java index fcfe017e..5167cc62 100644 --- a/src/main/java/lanat/utils/ErrorCallbacks.java +++ b/src/main/java/lanat/utils/ErrorCallbacks.java @@ -1,22 +1,28 @@ package lanat.utils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; +/** + * Interface for classes that have error and success callbacks. + * @param The type the success callback takes. + * @param The type the error callback takes. + */ public interface ErrorCallbacks { /** * Specify a function that will be called on error. * @param callback The function to be called on error. - * */ - void setOnErrorCallback(@NotNull Consumer<@NotNull TErr> callback); + */ + void setOnErrorCallback(@Nullable Consumer<@NotNull TErr> callback); /** * Specify a function that will be called on success. * @param callback The function to be called on success. - * */ - void setOnCorrectCallback(@NotNull Consumer<@NotNull TOk> callback); + */ + void setOnOkCallback(@Nullable Consumer<@NotNull TOk> callback); - /**Executes the correct or error callback. */ + /** Executes the correct or error callback. */ void invokeCallbacks(); } diff --git a/src/main/java/lanat/utils/ErrorLevelProvider.java b/src/main/java/lanat/utils/ErrorLevelProvider.java index b2a51c3f..45060444 100644 --- a/src/main/java/lanat/utils/ErrorLevelProvider.java +++ b/src/main/java/lanat/utils/ErrorLevelProvider.java @@ -3,6 +3,13 @@ import lanat.ErrorLevel; import org.jetbrains.annotations.NotNull; +/** + * Interface for classes that have an error level. + */ public interface ErrorLevelProvider { + /** + * Returns the error level of the object. + * @return The error level of the object. + */ @NotNull ErrorLevel getErrorLevel(); } diff --git a/src/main/java/lanat/utils/ErrorsContainer.java b/src/main/java/lanat/utils/ErrorsContainer.java index ab05fcb9..f7342507 100644 --- a/src/main/java/lanat/utils/ErrorsContainer.java +++ b/src/main/java/lanat/utils/ErrorsContainer.java @@ -5,21 +5,60 @@ import java.util.List; +/** + * Represents a container of errors. Methods are provided to get the errors under a certain level, and to check if + * there are errors under a certain level. + * @param the type of error level provider + */ public interface ErrorsContainer { + /** + * Returns a list of all the errors under the exit level defined in this error container. + * These are errors that would cause the program to exit. + * @return a list of all the errors under the exit level + */ @NotNull List<@NotNull T> getErrorsUnderExitLevel(); + /** + * Returns a list of all the errors under the display level defined in this error container. + * These are errors that would be displayed to the user. + * @return a list of all the errors under the display level + */ @NotNull List<@NotNull T> getErrorsUnderDisplayLevel(); + /** + * Returns {@code true} if there are errors under the exit level defined in this error container. + * @return {@code true} if there are exit errors + */ boolean hasExitErrors(); + /** + * Returns {@code true} if there are errors under the display level defined in this error container. + * @return {@code true} if there are display errors + */ boolean hasDisplayErrors(); + /** + * Sets the minimum level that errors should have in order to cause the program to exit. + * @param level the minimum exit error level + */ void setMinimumExitErrorLevel(@NotNull ErrorLevel level); + /** + * Returns the minimum level that errors should have in order to cause the program to exit. + * @return the minimum exit error level + */ @NotNull ModifyRecord<@NotNull ErrorLevel> getMinimumExitErrorLevel(); + /** + * Sets the minimum level that errors should have in order to be displayed to the user. + * @param level the minimum display error level + */ void setMinimumDisplayErrorLevel(@NotNull ErrorLevel level); + /** + * Returns the minimum level that errors should have in order to be displayed to the user. + * @return the minimum display error level + */ @NotNull ModifyRecord<@NotNull ErrorLevel> getMinimumDisplayErrorLevel(); } \ No newline at end of file diff --git a/src/main/java/lanat/utils/ErrorsContainerImpl.java b/src/main/java/lanat/utils/ErrorsContainerImpl.java index 4bc4792d..fb9c6be9 100644 --- a/src/main/java/lanat/utils/ErrorsContainerImpl.java +++ b/src/main/java/lanat/utils/ErrorsContainerImpl.java @@ -6,9 +6,15 @@ import java.util.ArrayList; import java.util.List; +/** + * A container for errors. This class is used to store errors and their respective minimum error levels. + * It also has methods for getting errors under the minimum error level. + * + * @param The type of the errors to store. + */ public abstract class ErrorsContainerImpl implements ErrorsContainer { - private @NotNull ModifyRecord minimumExitErrorLevel = new ModifyRecord<>(ErrorLevel.ERROR); - private @NotNull ModifyRecord minimumDisplayErrorLevel = new ModifyRecord<>(ErrorLevel.INFO); + private @NotNull ModifyRecord minimumExitErrorLevel = ModifyRecord.of(ErrorLevel.ERROR); + private @NotNull ModifyRecord minimumDisplayErrorLevel = ModifyRecord.of(ErrorLevel.INFO); private final @NotNull List errors = new ArrayList<>(); public ErrorsContainerImpl() {} diff --git a/src/main/java/lanat/utils/LoopPool.java b/src/main/java/lanat/utils/LoopPool.java index 6a38ad5c..4716a073 100644 --- a/src/main/java/lanat/utils/LoopPool.java +++ b/src/main/java/lanat/utils/LoopPool.java @@ -2,6 +2,15 @@ import org.jetbrains.annotations.NotNull; +/** + * A sequence of elements that can be looped through. The sequence can be looped through in both directions. + * This may be done by calling {@link #next()} or {@link #prev()}. + *

      + * An internal index is used to keep track of the current element. This index can be also be set manually by + * calling {@link #setIndex(int)}. + *

      + * @param The type of the elements in the sequence. + */ public class LoopPool { private final T @NotNull [] pool; private int index; @@ -12,43 +21,97 @@ private LoopPool(int startAt, T @NotNull ... pool) { this.setIndex(startAt); } + /** + * Creates a new {@link LoopPool} with the given elements. The index will start at 0. + * @param pool The elements to loop through. + * @return A new {@link LoopPool} with the given elements. + * @param The type of the elements. + */ @SafeVarargs - public static LoopPool of(T @NotNull ... pool) { + public static LoopPool of(T... pool) { return new LoopPool<>(0, pool); } + /** + * Creates a new {@link LoopPool} with the given elements. The index will start at the given index. + * @param startAt The index to start at. + * @param pool The elements to loop through. + * @return A new {@link LoopPool} with the given elements. + * @param The type of the elements. + */ @SafeVarargs - public static LoopPool of(int startAt, T @NotNull ... pool) { + public static LoopPool of(int startAt, T... pool) { return new LoopPool<>(startAt, pool); } + /** + * Creates a new {@link LoopPool} with the given elements. The index will start at a random index between 0 and the number + * of elements provided. + * @param pool The elements to loop through. + * @return A new {@link LoopPool} with the given elements. + * @param The type of the elements. + */ @SafeVarargs - public static LoopPool atRandomIndex(T @NotNull ... pool) { + public static LoopPool atRandomIndex(T... pool) { return new LoopPool<>(Random.randInt(pool.length), pool); } + /** + * Returns a valid index to use in the pool array. If the index goes out of bounds, it will always wrap around. + * @param index The index to validate. + * @return A valid index to use in the pool array. + */ private int getValidIndex(int index) { return index < 0 ? this.pool.length - 1 : index % this.pool.length; } - private void setIndex(int index) { + /** + * Sets the internal index to the given index. If the index goes out of bounds, it will always wrap around. + * @param index The index to set. + */ + public void setIndex(int index) { this.index = this.getValidIndex(index); } + /** + * Returns the internal index. + * @return The internal index. + */ + public int getIndex() { + return this.index; + } + + /** + * Returns the next element in the sequence and increments the internal index. + * @return The next element in the sequence. + */ public T next() { this.setIndex(this.index + 1); return this.current(); } + /** + * Returns the previous element in the sequence and decrements the internal index. + * @return The previous element in the sequence. + */ public T prev() { this.setIndex(this.index - 1); return this.current(); } + /** + * Returns the current element in the sequence. + * @return The current element in the sequence. + */ public T current() { return this.pool[this.index]; } + /** + * Returns the element at the given relative index. The index is relative to the internal index. + * @param relativeIndex The relative index. + * @return The element at the given relative index. + */ public T at(int relativeIndex) { return this.pool[this.getValidIndex(this.index + relativeIndex)]; } diff --git a/src/main/java/lanat/utils/ModifyRecord.java b/src/main/java/lanat/utils/ModifyRecord.java index 250b52df..a3b406d7 100644 --- a/src/main/java/lanat/utils/ModifyRecord.java +++ b/src/main/java/lanat/utils/ModifyRecord.java @@ -2,8 +2,6 @@ import org.jetbrains.annotations.NotNull; -import java.util.function.Supplier; - /** * Provides a way to see if the inner value has been modified since the constructor was called. * @@ -13,21 +11,50 @@ public class ModifyRecord { private T value; private boolean modified; - public ModifyRecord(T value) { + private ModifyRecord(T value) { this.value = value; } - public ModifyRecord() {} + /** + * Creates a new {@link ModifyRecord} with the given value. + * @param value The value to store. + * @return A new {@link ModifyRecord} with the given value. + * @param The type of the value. + */ + public static ModifyRecord of(@NotNull T value) { + return new ModifyRecord<>(value); + } + /** + * Creates a new empty {@link ModifyRecord}. + * @return A new empty {@link ModifyRecord}. + * @param The type of the value. + */ + public static ModifyRecord empty() { + return new ModifyRecord<>(null); + } + + /** + * Returns the value stored in the {@link ModifyRecord}. + * @return The value stored in the {@link ModifyRecord}. + */ public T get() { return this.value; } + /** + * Sets the value to the specified value and marks this {@link ModifyRecord} as modified. + * @param value The value to set. + */ public void set(T value) { this.value = value; this.modified = true; } + /** + * Sets the value to the value provided by the specified {@link ModifyRecord} and marks this {@link ModifyRecord} as modified. + * @param value The value to set. + */ public void set(@NotNull ModifyRecord value) { this.set(value.value); } @@ -45,6 +72,7 @@ public void setIfNotModified(T value) { /** * Sets the value to the specified value if it has not been modified. + * The value is provided by the specified {@link ModifyRecord}. * * @param value The value to set. */ @@ -55,22 +83,15 @@ public void setIfNotModified(@NotNull ModifyRecord value) { } /** - * Sets the value to the supplied value from the callback if it has not been modified. - * - * @param cb The callback that supplies the value to set. + * Returns {@code true} if the value has been modified. + * @return {@code true} if the value has been modified. */ - public void setIfNotModified(@NotNull Supplier cb) { - if (!this.modified) { - this.set(cb.get()); - } - } - public boolean isModified() { return this.modified; } @Override public String toString() { - return "[" + (this.modified ? "modified" : "clean") + "] " + (this.value == null ? "" : this.value.toString()); + return "[" + (this.modified ? "modified" : "clean") + "] " + (this.value == null ? "" : this.value); } } diff --git a/src/main/java/lanat/utils/Comparator.java b/src/main/java/lanat/utils/MultiComparator.java similarity index 70% rename from src/main/java/lanat/utils/Comparator.java rename to src/main/java/lanat/utils/MultiComparator.java index 66c38244..ce98c6b0 100644 --- a/src/main/java/lanat/utils/Comparator.java +++ b/src/main/java/lanat/utils/MultiComparator.java @@ -1,15 +1,23 @@ package lanat.utils; import java.util.ArrayList; +import java.util.Comparator; import java.util.function.Predicate; /** * A class that allows you to compare two objects using a list of predicates. + * * @param The type of the objects to compare. - * */ -public class Comparator { + */ +public class MultiComparator { private final ArrayList> predicates = new ArrayList<>(); + /** + * A record that stores a predicate and its priority. + * @param priority The priority of the predicate. + * @param predicateCb The predicate. + * @param The type of the object the predicate takes. + */ private record Pred(int priority, Predicate predicateCb) { Pred { if (priority < 0) throw new IllegalArgumentException("Priority must be >= 0"); @@ -19,31 +27,35 @@ private record Pred(int priority, Predicate predicateCb) { /** * Adds a predicate to the list of predicates to be used when comparing. + * * @param p The predicate to add. - * @param priority The priority of the predicate. The higher the priority, the earlier the predicate will be checked. + * @param priority The priority of the predicate. The higher the priority, the earlier the predicate will be + * checked. */ - public Comparator addPredicate(Predicate p, int priority) { + public MultiComparator addPredicate(Predicate p, int priority) { this.predicates.add(new Pred<>(priority, p)); return this; } /** * Adds a predicate to the list of predicates to be used when comparing. The priority of the predicate will be 0. + * * @param p The predicate to add. */ - public Comparator addPredicate(Predicate p) { + public MultiComparator addPredicate(Predicate p) { return this.addPredicate(p, 0); } /** * Compares the two objects given using the predicates added to this comparator. + * * @param first The first object to compare. * @param second The second object to compare. * @return -1 if the first object is "greater" than the second, 1 if the second object is "greater" than the first, - * 0 if they are equal. + * 0 if they are equal. */ public int compare(T first, T second) { - this.predicates.sort((a, b) -> b.priority - a.priority); + this.predicates.sort(Comparator.comparingInt(Pred::priority)); for (final Pred p : this.predicates) { if (p.predicateCb.test(first)) return -1; if (p.predicateCb.test(second)) return 1; diff --git a/src/main/java/lanat/utils/Pair.java b/src/main/java/lanat/utils/Pair.java index 39a1d967..ce4c268a 100644 --- a/src/main/java/lanat/utils/Pair.java +++ b/src/main/java/lanat/utils/Pair.java @@ -1,3 +1,8 @@ package lanat.utils; +/** + * A record that stores two objects. + * @param The type of the first object. + * @param The type of the second object. + */ public record Pair(TFirst first, TSecond second) {} \ No newline at end of file diff --git a/src/main/java/lanat/utils/Range.java b/src/main/java/lanat/utils/Range.java index d247247f..a4a3f5ef 100644 --- a/src/main/java/lanat/utils/Range.java +++ b/src/main/java/lanat/utils/Range.java @@ -5,14 +5,25 @@ /** A range class to contain a minimum and maximum value, or a single value. */ public class Range { + /** A range between 0 and infinity. */ public static final Range ANY = Range.from(0).toInfinity(); + /** A range between 1 and infinity. */ public static final Range AT_LEAST_ONE = Range.from(1).toInfinity(); + /** A range of 0. */ public static final Range NONE = Range.of(0); + /** A range of 1. */ public static final Range ONE = Range.of(1); private final int min, max; - public boolean isInfinite; + /** Whether the range is infinite. */ + public final boolean isInfinite; + /** + * Creates a new range with the given minimum and maximum values. If the maximum value is -1, the range max value + * will be infinity. + * @param min The minimum value + * @param max The maximum value, or -1 for infinity + */ private Range(int min, int max) { this.isInfinite = max == -1; if (min < 0) @@ -26,7 +37,7 @@ private Range(int min, int max) { public static class RangeBuilder { private final int min; - private int max = 0; + private int max; private RangeBuilder(int min) { this.min = min; @@ -34,6 +45,9 @@ private RangeBuilder(int min) { /** Sets the maximum value. */ public @NotNull Range to(int max) { + if (max < 0) + throw new IllegalArgumentException("max value cannot be negative"); + this.max = max; return this.build(); } @@ -60,11 +74,12 @@ private RangeBuilder(int min) { return new Range(value, value); } + /** Returns {@code true} if this is representing a range, and not a single value. */ public boolean isRange() { return this.min != this.max; } - /** Returns true if the range is 0. */ + /** Returns {@code true} if the range is 0. */ public boolean isZero() { return this.max == 0; } @@ -81,6 +96,7 @@ public int max() { /** * Returns a string representation of the range, such as "from 3 to 5 times", or "3 times". + * * @param kind The kind of thing the range is for, such as "time" or "argument" * @return The string representation */ @@ -91,21 +107,53 @@ public int max() { } /** - * Returns a string representation of the range, such as "{3, 5}" or "{3}". - * If the max value is {@link Short#MAX_VALUE}, it will be represented as "...". + * Returns a string representation of the range, such as "{3, 5}" or "{3}". If the max + * value is {@link Short#MAX_VALUE}, it will be represented as "...". + * * @return The string representation */ public @NotNull String getRegexRange() { return this.isRange() - ? "{%d, %s}".formatted(this.min, "" + (this.max == -1 ? "..." : this.max)) + ? "{%d, %s}".formatted(this.min, "" + (this.isInfinite ? "..." : this.max)) : "{%d}".formatted(this.min); } - public boolean isInRange(int value) { - return value >= this.min && (this.isInfinite || value <= this.max); + /** + * Returns {@code true} if the given value is in the range. + * @param value The value to check + * @param minInclusive Whether the minimum value is inclusive + * @param maxInclusive Whether the maximum value is inclusive + * @return {@code true} if the value is in the range + */ + public boolean isInRange(int value, boolean minInclusive, boolean maxInclusive) { + boolean isInMin = minInclusive + ? value >= this.min + : value > this.min; + + // if the max value is infinity, it will always be below the max + boolean isInMax = this.isInfinite || (maxInclusive + ? value <= this.max + : value < this.max + ); + + return isInMin && isInMax; } - public boolean isIndexInRange(int value) { - return value >= 0 && (this.isInfinite || value < this.max); + /** + * Returns {@code true} if the given value is in the range, inclusive. + * @param value The value to check + * @return {@code true} if the value is in the range + */ + public boolean isInRangeInclusive(int value) { + return this.isInRange(value, true, true); + } + + /** + * Returns {@code true} if the given value is in the range, exclusive. + * @param value The value to check + * @return {@code true} if the value is in the range + */ + public boolean isInRangeExclusive(int value) { + return this.isInRange(value, false, false); } } \ No newline at end of file diff --git a/src/main/java/lanat/utils/Resettable.java b/src/main/java/lanat/utils/Resettable.java index e5492d8b..4370632c 100644 --- a/src/main/java/lanat/utils/Resettable.java +++ b/src/main/java/lanat/utils/Resettable.java @@ -1,5 +1,8 @@ package lanat.utils; +/** + * Represents an object that can be reset to its default/initial state. + */ public interface Resettable { /** * Resets the object to its default/initial state. diff --git a/src/main/java/lanat/utils/UtlMisc.java b/src/main/java/lanat/utils/UtlMisc.java new file mode 100644 index 00000000..c7af99f7 --- /dev/null +++ b/src/main/java/lanat/utils/UtlMisc.java @@ -0,0 +1,47 @@ +package lanat.utils; + +import lanat.CommandUser; +import lanat.MultipleNamesAndDescription; +import lanat.exceptions.ObjectAlreadyExistsException; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Function; + +public final class UtlMisc { + private UtlMisc() {} + + /** + * Checks if the elements in the given list are unique. + * @param list The list to check + * @param exceptionSupplier A function that takes the duplicate element and returns an exception to throw + * @param The type of the elements in the list + */ + public static void requireUniqueElements( + @NotNull List list, + @NotNull Function exceptionSupplier + ) { + for (int i = 0; i < list.size(); i++) { + final var el = list.get(i); + + for (int j = i + 1; j < list.size(); j++) { + final var other = list.get(j); + + if (el.equals(other)) + throw exceptionSupplier.apply(other); + } + } + } + + /** + * Returns {@code true} if the given objects have the same names and parent command. + * @param a The first object + * @param b The second object + * @return {@code true} if the given objects have the same names and parent command + * @param The type of the objects + */ + public static + boolean equalsByNamesAndParentCmd(@NotNull T a, @NotNull T b) { + return a.getParentCommand() == b.getParentCommand() && a.getNames().stream().anyMatch(b::hasName); + } +} diff --git a/src/main/java/lanat/utils/UtlReflection.java b/src/main/java/lanat/utils/UtlReflection.java index f424bc3a..a1e8714e 100644 --- a/src/main/java/lanat/utils/UtlReflection.java +++ b/src/main/java/lanat/utils/UtlReflection.java @@ -2,11 +2,15 @@ import org.jetbrains.annotations.NotNull; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.stream.Stream; + public final class UtlReflection { private UtlReflection() {} /** - * This method returns the simple name of the given class. If the class is an anonymous class, then the simple name + * Returns the simple name of the given class. If the class is an anonymous class, then the simple name * of the superclass is returned. * * @param clazz The class to get the simple name of. @@ -21,4 +25,44 @@ private UtlReflection() {} return name; } + + /** + * Returns whether the given method has the given parameters in the given order. + * + * @param method The method to check. + * @param parameters The parameters to check. + * @return Whether the given method has the given parameters in the given order. + */ + public static boolean hasParameters(Method method, Class... parameters) { + return Arrays.equals(method.getParameterTypes(), parameters); + } + + /** + * Instantiates the given class with the given arguments. + * + * @param clazz The class to instantiate. + * @param args The arguments to pass to the constructor. + * @param The type of the class. + * @return The instantiated class. If the class could not be instantiated, a {@link RuntimeException} is thrown. + */ + public static T instantiate(Class clazz, Object... args) { + try { + return clazz.getDeclaredConstructor().newInstance(args); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a stream of all methods in the given class. + * If the given class is an anonymous class, then the methods of the superclass are returned. + * @param clazz The class to get the methods of. + * @return A stream of all methods in the given class. + */ + public static Stream getMethods(Class clazz) { + if (clazz.isAnonymousClass()) + return UtlReflection.getMethods(clazz.getSuperclass()); + return Stream.of(clazz.getDeclaredMethods()); + } + } diff --git a/src/main/java/lanat/utils/UtlString.java b/src/main/java/lanat/utils/UtlString.java index 24808341..ab91ece4 100644 --- a/src/main/java/lanat/utils/UtlString.java +++ b/src/main/java/lanat/utils/UtlString.java @@ -1,32 +1,21 @@ package lanat.utils; +import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Arrays; -import java.util.function.Predicate; +import java.util.regex.Pattern; public final class UtlString { private UtlString() {} /** - * Apply a predicate for each character in the string, if any fails, return false. - * - * @param str The string to check. - * @param fn The predicate to apply to each character. - */ - public static boolean matchCharacters(@NotNull String str, @NotNull Predicate fn) { - for (char chr : str.toCharArray()) { - if (!fn.test(chr)) return false; - } - return true; - } - - /** - * Wrap a string in two strings at both sides. + * Wrap a string in two strings at both sides. If the string is {@code null}, it will be replaced with + * "null". */ public static @NotNull String surround(@Nullable String str, @NotNull String wrapper) { - return wrapper + str + wrapper; + return str == null ? "null" : wrapper + str + wrapper; } /** @@ -43,20 +32,27 @@ public static boolean matchCharacters(@NotNull String str, @NotNull Predicate b.length() - a.length()).orElse(""); } - public static @NotNull String sanitizeName(@NotNull String name) { - // remove all non-alphanumeric characters - final var sanitized = UtlString.trim(name.replaceAll("[^a-zA-Z0-9 -]", ""), "[^a-zA-Z0-9]") - .replaceAll(" ", "-"); + /** + * Check if a string is a valid name for it to be used in an element. + * @param name The name to check. + * @throws IllegalArgumentException if the name is invalid. + */ + public static @NotNull String requireValidName(@NotNull String name) { + if (name.length() == 0) + throw new IllegalArgumentException("name must contain at least one character"); - if (sanitized.isEmpty()) - throw new IllegalArgumentException("name must contain at least one alphanumeric character"); + if (!Character.isAlphabetic(name.charAt(0))) + throw new IllegalArgumentException("name must start with an alphabetic character or an underscore"); - return sanitized; + if (!name.matches("[a-zA-Z0-9_-]+")) + throw new IllegalArgumentException("name must only contain alphanumeric characters and underscores and dashes"); + + return name; } /** - * Wraps a string into multiple lines in order to fit in the given maximum width. Wrapping respects words, so no - * word will be split in two lines. + * Wraps a string into multiple lines in order to fit in the given maximum width. Wrapping respects words and + * indentation, so no word will be split in two lines and indentation will be preserved. * * @param str The text to wrap. * @param maxWidth The maximum width that the text should never exceed. @@ -66,8 +62,9 @@ public static boolean matchCharacters(@NotNull String str, @NotNull Predicate + * If the string is longer than the width, it is returned as is. + *

      + * @param str The string to center. + * @param width The width to center the string in. + * @param padChar The character to use for padding. + * @return The centered string. + */ public static @NotNull String center(@NotNull String str, int width, char padChar) { - final var buffer = new StringBuilder(); - final var paddingString = String.valueOf(padChar).repeat((width / 2) - (str.length() / 2) - 1); + if (str.length() >= width) + return str; - buffer.append(paddingString); - buffer.append(str); - buffer.append(paddingString); + final var paddingString = String.valueOf(padChar).repeat((width / 2) - (str.length() / 2) - 1); - return buffer.toString(); + return paddingString + str + paddingString; } + /** + * Centers a string in a given width. '-' is used to fill the remaining space. + * @param str The string to center. + * @param width The width to center the string in. + * @return The centered string. + * @see UtlString#center(String, int, char) + */ public static @NotNull String center(@NotNull String str, int width) { - return UtlString.center(str, width, '─'); + return UtlString.center(str, width, '-'); } - public static @NotNull String trim(@NotNull String str, @NotNull String regex) { - return str.replaceAll("^" + regex + "+", "") - .replaceAll(regex + "+$", ""); - } - - public static @NotNull String trim(@NotNull String str) { - return UtlString.trim(str, "[ \n\r\t]"); - } - - /** * Remove all formatting colors or format from the string */ public static @NotNull String removeSequences(@NotNull String str) { - return str.replaceAll("\033\\[[\\d;]*m", ""); + return str.replaceAll(TextFormatter.ESC + "\\[[\\d;]*m", ""); } /** - * Returns the count given appended to the string given. An 's' will be appended at the end if - * the count is not 1. + * Returns the count given appended to the string given. An 's' will be appended at the end if the + * count is not 1. + * * @param str the string to append to * @param count the count * @return "count str" or "count strs" depending on the count @@ -207,20 +210,46 @@ public static boolean matchCharacters(@NotNull String str, @NotNull Predicate BRIGHT_COLORS = List.of( + BRIGHT_RED, + BRIGHT_GREEN, + BRIGHT_YELLOW, + BRIGHT_BLUE, + BRIGHT_MAGENTA, + BRIGHT_CYAN, + BRIGHT_WHITE + ); + + /** + * Immutable list of all the bright colors. + */ + public static final @NotNull List DARK_COLORS = List.of( + RED, + GREEN, + YELLOW, + BLUE, + MAGENTA, + CYAN, + WHITE + ); } \ No newline at end of file diff --git a/src/main/java/lanat/utils/displayFormatter/FormatOption.java b/src/main/java/lanat/utils/displayFormatter/FormatOption.java index 4c8fba8d..64789ee0 100644 --- a/src/main/java/lanat/utils/displayFormatter/FormatOption.java +++ b/src/main/java/lanat/utils/displayFormatter/FormatOption.java @@ -2,14 +2,25 @@ import org.jetbrains.annotations.NotNull; +/** + * Enumerates the ANSI format codes that a terminal can normally display. + *

      + * Please note that compatibility with all terminals may vary. + *

      + */ public enum FormatOption { + /** Resets all formatting. Including colors. */ RESET_ALL(0), BOLD(1), ITALIC(3), + /** Makes the text dimmer. */ DIM(2), UNDERLINE(4), + /** Makes the text blink. */ BLINK(5), + /** Reverses the foreground and background colors. */ REVERSE(7), + /** Hides the text. */ HIDDEN(8), STRIKE_THROUGH(9); @@ -19,12 +30,30 @@ public enum FormatOption { this.value = (byte)value; } + /** + * Returns the ANSI escape sequence for this format option. + * @return The ANSI escape sequence for this format option. + * @see FormatOption#seq() + */ @Override public @NotNull String toString() { + return this.seq(); + } + + /** + * Returns the ANSI escape sequence for this format option. + * @return The ANSI escape sequence for this format option. + */ + public @NotNull String seq() { return TextFormatter.getSequence(this.value); } - public @NotNull String toStringReset() { + /** + * Returns the ANSI escape sequence which resets the formatting of this option. + * @return The ANSI escape sequence which resets the formatting of this option. + */ + public @NotNull String reset() { + // for some reason, bold is 21 instead of 20 return TextFormatter.getSequence(this.value + 20 + (this == BOLD ? 1 : 0)); } } \ No newline at end of file diff --git a/src/main/java/lanat/utils/displayFormatter/TextFormatter.java b/src/main/java/lanat/utils/displayFormatter/TextFormatter.java index cd29bb6a..c0e8fb77 100644 --- a/src/main/java/lanat/utils/displayFormatter/TextFormatter.java +++ b/src/main/java/lanat/utils/displayFormatter/TextFormatter.java @@ -7,59 +7,119 @@ import java.util.Arrays; import java.util.List; +/** + * Allows to easily format text for display in a terminal. + *

      + * Multiple formatters can be concatenated together. This is useful for when you want to + * format a string that has multiple parts that need to be formatted differently. + *

      + */ public class TextFormatter { - public static boolean enableSequences = true, debug = false; + /** + * When set to {@code false}, no formatting will be applied to text. Raw text will be generated without any + * color or formatting. + */ + public static boolean enableSequences = true; + + /** + * When set to {@code true}, the {@link #toString()} method will not add any terminal sequences, but rather + * return the sequences that would be added by marking them as {@code ESC[} + */ + public static boolean debug = false; + + /** A list of all the formatting options that should be applied to the text. */ private final @NotNull ArrayList formatOptions = new ArrayList<>(); + + /** A list of all the formatters that should be concatenated to this formatter. */ private final @NotNull List concatList = new ArrayList<>(); + + /** The parent formatter. Used when being concatenated to another formatter. */ private @Nullable TextFormatter parent; private @Nullable Color foregroundColor; private @Nullable Color backgroundColor; private @NotNull String contents; + /** + * Creates a new {@link TextFormatter} with the specified contents. + * @param contents The contents of the formatter. + */ public TextFormatter(@NotNull String contents) { this.contents = contents; } + /** + * Creates a new {@link TextFormatter} with the specified contents and foreground color. + * @param contents The contents of the formatter. + * @param foreground The foreground color of the formatter. + */ public TextFormatter(@NotNull String contents, @Nullable Color foreground) { this(contents); this.foregroundColor = foreground; } + /** + * Creates a new {@link TextFormatter} with no contents. + */ public TextFormatter() { this.contents = ""; } + /** + * Adds the specified formatting options to the formatter. + * @param options The formatting options to add. + */ public TextFormatter addFormat(@NotNull FormatOption... options) { this.formatOptions.addAll(Arrays.asList(options)); return this; } - public TextFormatter setColor(@Nullable Color foreground) { + /** + * Sets the foreground color of the formatter. + * @param foreground The foreground color of the formatter. + */ + public TextFormatter withForegroundColor(@Nullable Color foreground) { this.foregroundColor = foreground; return this; } - public TextFormatter setColor(@Nullable Color foreground, @Nullable Color background) { - this.foregroundColor = foreground; + /** + * Sets the background color of the formatter. + * @param background The background color of the formatter. + */ + public TextFormatter withBackgroundColor(@Nullable Color background) { this.backgroundColor = background; return this; } - public TextFormatter setBackgroundColor(@Nullable Color background) { + /** + * Sets the foreground and background color of the formatter. + * @param foreground The foreground color of the formatter. + * @param background The background color of the formatter. + */ + public TextFormatter withColors(@Nullable Color foreground, @Nullable Color background) { + this.foregroundColor = foreground; this.backgroundColor = background; return this; } - public TextFormatter setContents(@NotNull String contents) { + /** + * Sets the contents of the formatter. + * @param contents The contents of the formatter. + */ + public TextFormatter withContents(@NotNull String contents) { this.contents = contents; return this; } + /** + * Concatenates the specified formatters to this formatter. + * @param formatters The formatters to concatenate. + */ public TextFormatter concat(@NotNull TextFormatter... formatters) { for (TextFormatter formatter : formatters) { - // if it was already added to another formatter, remove it from there + // if it was already added to another formatter, throw an exception if (formatter.parent != null) { - formatter.parent.concatList.remove(formatter); + throw new IllegalArgumentException("Cannot concatenate a formatter that is already concatenated to another formatter."); } formatter.parent = this; this.concatList.add(formatter); @@ -67,6 +127,10 @@ public TextFormatter concat(@NotNull TextFormatter... formatters) { return this; } + /** + * Concatenates the specified strings to this formatter. + * @param strings The strings to concatenate. + */ public TextFormatter concat(@NotNull String... strings) { for (var str : strings) { this.concatList.add(new TextFormatter(str)); @@ -74,6 +138,11 @@ public TextFormatter concat(@NotNull String... strings) { return this; } + /** + * Returns whether the formatter is simple. A formatter is simple if it has no formatting options, no foreground + * color, no background color, and no concatenated formatters. + * @return {@code true} if the formatter is simple + */ public boolean isSimple() { return ( this.contents.length() == 0 @@ -82,6 +151,10 @@ public boolean isSimple() { ) && this.concatList.size() == 0; // we cant skip if we need to concat stuff! } + /** + * Returns whether the formatter has no formatting options, no foreground color, and no background color. + * @return {@code true} if the formatter has no formatting options, no foreground color, and no background color + */ public boolean formattingNotDefined() { return ( this.foregroundColor == null @@ -90,6 +163,11 @@ public boolean formattingNotDefined() { ); } + /** + * Returns the start sequences to add to the contents of the formatter. This includes the foreground color, the + * background color, and all the formatting options. + * @return the start sequences to add to the contents of the formatter + */ private @NotNull String getStartSequences() { if (this.formattingNotDefined() || !TextFormatter.enableSequences) return ""; final var buffer = new StringBuilder(); @@ -97,7 +175,7 @@ public boolean formattingNotDefined() { if (this.foregroundColor != null) buffer.append(this.foregroundColor); if (this.backgroundColor != null) - buffer.append(this.backgroundColor.toStringBackground()); + buffer.append(this.backgroundColor.bg()); for (var option : this.formatOptions) { buffer.append(option); @@ -111,14 +189,22 @@ public boolean formattingNotDefined() { final var buffer = new StringBuilder(); if (this.backgroundColor != null) { - return buffer.append(FormatOption.RESET_ALL).toString(); + var bgColor = this.getResetBgColor(); + + // if bg color is null, we can just reset everything then. + // also, it's not worth it to add any resetting sequences afterward, so just return this directly. + if (bgColor == null) + return FormatOption.RESET_ALL.seq(); + + buffer.append(bgColor.bg()); } for (var option : this.formatOptions) { - buffer.append(option.toStringReset()); + buffer.append(option.reset()); } + if (this.foregroundColor != null) { - buffer.append(this.getResetColor()); + buffer.append(this.getResetFgColor()); } return buffer.toString(); @@ -127,16 +213,31 @@ public boolean formattingNotDefined() { /** * Returns the {@link Color} that should properly reset the foreground color. This is determined by looking at the * parent formatters. If no parent formatter has a foreground color, then {@link Color#BRIGHT_WHITE} is returned. + * @return the {@link Color} that should properly reset the foreground color */ - private @NotNull Color getResetColor() { - var parent = this.parent; - while (parent != null) { - if (parent.foregroundColor != null) { - return parent.foregroundColor; - } - parent = parent.parent; - } - return Color.BRIGHT_WHITE; + private @NotNull Color getResetFgColor() { + if (this.parent == null) + return Color.BRIGHT_WHITE; + + if (this.parent.foregroundColor != null) + return this.parent.foregroundColor; + + return this.parent.getResetFgColor(); + } + + /** + * Returns the {@link Color} that should properly reset the background color. This is determined by looking at the + * parent formatters. If no parent formatter has a background color, then {@code null} is returned. + * @return the {@link Color} that should properly reset the background color + */ + private @Nullable Color getResetBgColor() { + if (this.parent == null) + return null; + + if (this.parent.backgroundColor != null) + return this.parent.backgroundColor; + + return this.parent.getResetBgColor(); } /** @@ -164,7 +265,7 @@ public boolean formattingNotDefined() { /** Returns a template for a {@link TextFormatter} that is used for errors */ public static @NotNull TextFormatter ERROR(@NotNull String msg) { - return new TextFormatter(msg).setColor(Color.BRIGHT_RED).addFormat(FormatOption.REVERSE, FormatOption.BOLD); + return new TextFormatter(msg, Color.BRIGHT_RED).addFormat(FormatOption.REVERSE, FormatOption.BOLD); } public static @NotNull String getSequence(int code) { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 992c4545..a408247e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,17 +1,14 @@ module lanat { requires org.jetbrains.annotations; - requires fade.mirror; - exports lanat; + exports lanat; exports lanat.argumentTypes; exports lanat.utils.displayFormatter; exports lanat.helpRepresentation; exports lanat.helpRepresentation.descriptions; exports lanat.helpRepresentation.descriptions.exceptions; exports lanat.parsing; + exports lanat.parsing.errors; exports lanat.utils; exports lanat.exceptions; - - opens lanat.parsing.errors to fade.mirror; - opens lanat.commandTemplates to fade.mirror; } \ No newline at end of file diff --git a/src/test/java/lanat/test/ManualTests.java b/src/test/java/lanat/test/ManualTests.java deleted file mode 100644 index b3578e71..00000000 --- a/src/test/java/lanat/test/ManualTests.java +++ /dev/null @@ -1,76 +0,0 @@ -package lanat.test; - -import lanat.Argument; -import lanat.ArgumentGroup; -import lanat.ArgumentType; -import lanat.Command; -import lanat.argumentTypes.Parseable; -import lanat.utils.Range; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; - -public final class ManualTests { - @Test - public void main() { -// HelpFormatter.lineWrapMax = 110; -// HelpFormatter.debugLayout = true; -// TextFormatter.debug = true; - - enum TestEnum { - ONE, TWO, THREE - } - - var parser = new TestingParser("Testing", "" - + "hello, the argument is formatted! " - + "This is its type description: " - ) {{ - this.addArgument(Argument.create("testing", ArgumentType.FROM_PARSEABLE(new TestClass())) - .description("some description") - .onOk(value -> System.out.println("ok: " + value)) - ); - - this.addGroup(new ArgumentGroup("group") {{ - this.exclusive(); - this.addArgument(Argument.create("group-arg", ArgumentType.STRING()) - .onOk(value -> System.out.println("1: " + value)) - .description("some description") - ); - this.addArgument(Argument.create("group-arg2", ArgumentType.ENUM(TestEnum.ONE)) - .onOk(value -> System.out.println("2: " + value)) - .description("") - ); - }}); - - this.addSubCommand(new Command("hello", "Some description for the command") {{ - this.addNames("hi", "hey"); - this.addArgument(Argument.create("world", ArgumentType.INTEGER_RANGE(5, 10)) - .onOk(value -> System.out.println("ok: " + value)) - ); - }}); - - this.addSubCommand(new Command("goodbye", "Some description for this other command") {{ - this.addNames("bye", "cya"); - this.addArgument(Argument.create("world", new StringJoiner()) - .onOk(value -> System.out.println("ok: " + value)) - ); - }}); - }}; - - parser.parseArgs(""); - } -} - - -class TestClass implements Parseable { - - @Override - public @NotNull Range getRequiredArgValueCount() { - return Range.ONE; - } - - @Override - public @Nullable Integer parseValues(@NotNull String @NotNull [] args) { - return Integer.parseInt(args[0]); - } -} \ No newline at end of file diff --git a/src/test/java/lanat/test/TestArgumentGroups.java b/src/test/java/lanat/test/TestArgumentGroups.java deleted file mode 100644 index d9e62303..00000000 --- a/src/test/java/lanat/test/TestArgumentGroups.java +++ /dev/null @@ -1,29 +0,0 @@ -package lanat.test; - -import lanat.Argument; -import lanat.ArgumentGroup; -import lanat.ArgumentType; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class TestArgumentGroups extends UnitTests { - @Override - public void setParser() { - super.setParser(); - this.parser.addGroup(new ArgumentGroup("group") {{ - this.exclusive(); - this.addArgument(Argument.create("group-arg", ArgumentType.BOOLEAN())); - this.addArgument(Argument.create("group-arg2", ArgumentType.BOOLEAN())); - }}); - } - - @Test - @DisplayName("Test exclusive group") - public void testExclusiveGroup() { - var parsedArgs = this.parser.parseArgs("--group-arg --group-arg2"); - assertEquals(Boolean.TRUE, parsedArgs.get("group-arg").get()); - assertEquals(Boolean.FALSE, parsedArgs.get("group-arg2").get()); // group-arg2 should not be present - } -} \ No newline at end of file diff --git a/src/test/java/lanat/test/TestParsedValues.java b/src/test/java/lanat/test/TestParsedValues.java deleted file mode 100644 index 09ffd555..00000000 --- a/src/test/java/lanat/test/TestParsedValues.java +++ /dev/null @@ -1,116 +0,0 @@ -package lanat.test; - -import lanat.ParsedArgumentsRoot; -import lanat.exceptions.ArgumentNotFoundException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; - -public class TestParsedValues extends UnitTests { - private ParsedArgumentsRoot parseArgs(String args) { - return this.parser.parseArgs(args); - } - - @Test - @DisplayName("Test the get() method") - public void testGetSimple() { - assertEquals("(hello), (world)", this.parseArgs("--what hello world").get("what").get()); - } - - @Test - @DisplayName("Exception thrown when querying an invalid argument") - public void testUnknownArg() { - assertThrows( - ArgumentNotFoundException.class, - () -> this.parseArgs("--what hello world").get("not-there") - ); - } - - @Test - @DisplayName("Test querying parsed values from arguments in subCommands") - public void testNestedArguments() { - var parsedArgs = this.parseArgs("smth subCommand -cccc another 56"); - assertEquals(4, parsedArgs.get("subCommand.c").get()); - assertEquals(4, parsedArgs.get("subCommand", "c").get()); - - assertEquals(56, parsedArgs.get("subCommand.another.number").get()); - assertEquals(56, parsedArgs.get("subCommand", "another", "number").get()); - } - - @Test - @DisplayName("Test the defined() callbacks") - public void testDefinedCallbacks() { - var parsedArgs = this.parseArgs("smth subCommand -cccc"); - final byte[] called = { 0 }; - - parsedArgs.get("subCommand.c").defined(v -> { - assertEquals(4, v); - called[0]++; - }); - - parsedArgs.get("subCommand.another.number").undefined(() -> called[0]++); - - assertEquals(2, called[0]); - } - - @Test - @DisplayName("Test the toOptional() method") - public void testToOptional() { - var parsedArgs = this.parseArgs("smth subCommand -cccc"); - parsedArgs.get("subCommand.c").asOptional().ifPresent(v -> assertEquals(4, v)); - parsedArgs.get("subCommand.another.number").asOptional().ifPresent(v -> assertEquals(56, v)); - assertTrue(parsedArgs.get("subCommand.c").asOptional().isPresent()); - } - - @Test - @DisplayName("Test the defined() overload for single-value arrays") - public void testArrayDefinedMethod() { - var parsedArgs = this.parseArgs("foo bar qux"); - - { - String[] value = new String[1]; - - if (parsedArgs.get("what").defined(value)) { - assertEquals(value[0], "(foo), (bar), (qux)"); - } else { - fail("The value was not defined"); - } - } - - { - String[] value = new String[1]; - - if (parsedArgs.get("a").defined(value)) { - fail("The value was defined"); - } - assertNull(value[0]); - } - - { - String[] value = new String[4]; - - assertThrows( - IllegalArgumentException.class, - () -> parsedArgs.get("what").defined(value) - ); - } - } - - @Test - @DisplayName("Test the forward value") - public void testForwardValue() { - { - Optional parsedArgs = this.parseArgs("foo -- hello world").getForwardValue(); - assertTrue(parsedArgs.isPresent()); - assertEquals("hello world", parsedArgs.get()); - } - - { - Optional parsedArgs = this.parseArgs("foo").getForwardValue(); - assertFalse(parsedArgs.isPresent()); - } - } -} \ No newline at end of file diff --git a/src/test/java/lanat/test/TestingParser.java b/src/test/java/lanat/test/TestingParser.java new file mode 100644 index 00000000..c0f70263 --- /dev/null +++ b/src/test/java/lanat/test/TestingParser.java @@ -0,0 +1,35 @@ +package lanat.test; + +import lanat.ArgumentParser; +import lanat.CLInput; +import lanat.CommandTemplate; +import lanat.ParsedArgumentsRoot; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class TestingParser extends ArgumentParser { + public TestingParser(String programName, String description) { + super(programName, description); + } + + public TestingParser(String programName) { + super(programName); + } + + public TestingParser(@NotNull Class templateClass) { + super(templateClass); + } + + public List parseGetErrors(String args) { + return this.parse(CLInput.from(args)).getErrors(); + } + + public @NotNull ParsedArgumentsRoot parseGetValues(@NotNull String args) { + var res = this.parse(CLInput.from(args)).getParsedArguments(); + assertNotNull(res, "The result of the parsing was null (Arguments have failed)"); + return res; + } +} diff --git a/src/test/java/lanat/test/UnitTests.java b/src/test/java/lanat/test/UnitTests.java index e2066d04..34caf87b 100644 --- a/src/test/java/lanat/test/UnitTests.java +++ b/src/test/java/lanat/test/UnitTests.java @@ -1,6 +1,11 @@ package lanat.test; -import lanat.*; +import lanat.Argument; +import lanat.ArgumentType; +import lanat.Command; +import lanat.argumentTypes.CounterArgumentType; +import lanat.argumentTypes.IntegerArgumentType; +import lanat.argumentTypes.StringArgumentType; import lanat.argumentTypes.TupleArgumentType; import lanat.helpRepresentation.HelpFormatter; import lanat.utils.Range; @@ -9,10 +14,7 @@ import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class StringJoiner extends TupleArgumentType { @@ -43,34 +45,6 @@ public RestrictedDoubleAdder() { } -class TestingParser extends ArgumentParser { - public TestingParser(String programName, String description) { - super(programName, description); - } - - public TestingParser(String programName) { - super(programName); - } - - public List parseArgsExpectError(String args) { - return this.parseArgsNoExit(args).second(); - } - - public ParsedArgumentsRoot parseArgsExpectErrorPrint(String args) { - final var parsed = this.parseArgsNoExit(args); - System.out.println(String.join("\n", parsed.second())); - return parsed.first(); - } - - @Override - public @NotNull ParsedArgumentsRoot parseArgs(@NotNull String args) { - var res = this.parseArgsNoExit(args).first(); - assertNotNull(res, "The result of the parsing was null (Arguments have failed)"); - return res; - } -} - - public class UnitTests { protected TestingParser parser; @@ -79,28 +53,38 @@ public class UnitTests { TextFormatter.enableSequences = false; // just so we don't have to worry about color codes } - public void setParser() { - this.parser = new TestingParser("Testing") {{ - this.addArgument(Argument.create("what", new StringJoiner()) + protected TestingParser setParser() { + return new TestingParser("Testing") {{ + this.setErrorCode(0b0100); + this.addArgument(Argument.create(new StringJoiner(), "what") .positional() .obligatory() ); - this.addArgument(Argument.create("double-adder", new RestrictedDoubleAdder())); - this.addArgument(Argument.create("a", ArgumentType.STRING())); - this.addSubCommand(new Command("subCommand") {{ - this.addArgument(Argument.create("c", ArgumentType.COUNTER())); - this.addArgument(Argument.create('s', "more-strings", new StringJoiner())); - this.addSubCommand(new Command("another") {{ - this.addArgument(Argument.create("ball", new StringJoiner())); - this.addArgument(Argument.create("number", ArgumentType.INTEGER()).positional().obligatory()); + this.addArgument(Argument.create(new RestrictedDoubleAdder(), "double-adder")); + this.addArgument(Argument.create(new StringArgumentType(), "a")); + + this.addCommand(new Command("subCommand") {{ + this.setErrorCode(0b0010); + this.addArgument(Argument.create(new CounterArgumentType(), "c")); + this.addArgument(Argument.create(new StringJoiner(), 's', "more-strings")); + + this.addCommand(new Command("another") {{ + this.setErrorCode(0b0001); + this.addArgument(Argument.create(new StringJoiner(), "ball")); + this.addArgument(Argument.create(new IntegerArgumentType(), "number").positional().obligatory()); }}); }}); + + this.addCommand(new Command("subCommand2") {{ + this.setErrorCode(0b1000); + this.addArgument(Argument.create(new IntegerArgumentType(), 'c').positional()); + }}); }}; } @BeforeEach public final void setup() { - this.setParser(); + this.parser = this.setParser(); } /** @@ -110,7 +94,7 @@ public final void setup() { * */ protected T parseArg(@NotNull String arg, @NotNull String values) { - return this.parser.parseArgs("--%s %s".formatted(arg.trim(), values)).get(arg).get(); + return this.parser.parseGetValues("--%s %s".formatted(arg.strip(), values)).get(arg).orElse(null); } /** @@ -120,6 +104,6 @@ protected T parseArg(@NotNull String arg, @NotNull String values) { * */ protected void assertNotPresent(@NotNull String arg) { - assertNull(this.parser.parseArgs("").get(arg).get()); + assertTrue(this.parser.parseGetValues("").get(arg).isEmpty()); } } \ No newline at end of file diff --git a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java new file mode 100644 index 00000000..68571df6 --- /dev/null +++ b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java @@ -0,0 +1,72 @@ +package lanat.test.manualTests; + +import lanat.Argument; +import lanat.ArgumentGroup; +import lanat.Command; +import lanat.CommandTemplate; +import lanat.argumentTypes.CounterArgumentType; +import lanat.argumentTypes.NumberRangeArgumentType; +import lanat.argumentTypes.StdinArgumentType; +import lanat.argumentTypes.StringArgumentType; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +@Command.Define(names = "my-program", description = "This is a test program.") +public class CommandTemplateExample extends CommandTemplate.Default { + public CommandTemplateExample() {} + + @Argument.Define(argType = StringArgumentType.class, description = "This is a string argument.") + public Optional string; + + @Argument.Define(description = "") + public double number = 12; + + @Argument.Define(argType = StdinArgumentType.class) + public String stdin; + + @Argument.Define(names = "arg1", argType = StringArgumentType.class) + public String arg1; + + + @Argument.Define(names = "arg1a", argType = StringArgumentType.class) + public String arg1copy; + + + @CommandAccessor + public MySubCommand subCommand; + + @InitDef + public static void beforeInit(@NotNull CommandBuildHelper helper) { + helper., Double>arg("number") + .withArgType(new NumberRangeArgumentType<>(5.5, 15.89)); + } + + @InitDef + public static void afterInit(@NotNull Command cmd) { + cmd.addGroup(new ArgumentGroup("test-group") {{ + this.addArgument(cmd.getArgument("string")); + this.addArgument(cmd.getArgument("number")); + }}); + } + + + @Command.Define(names = "sub-command", description = "This is a sub-command.") + public static class MySubCommand extends CommandTemplate { + public MySubCommand() {} + + @Argument.Define(argType = CounterArgumentType.class, description = "This is a counter", names = "c") + public int counter = 0; + + @CommandAccessor + public AnotherSubCommand anotherSubCommand; + + @Command.Define(names = "another-sub-command", description = "This is a sub-command.") + public static class AnotherSubCommand extends CommandTemplate { + public AnotherSubCommand() {} + + @Argument.Define(argType = CounterArgumentType.class, description = "This is a counter", names = "c") + public int counter = 0; + } + } +} \ No newline at end of file diff --git a/src/test/java/lanat/test/manualTests/ManualTests.java b/src/test/java/lanat/test/manualTests/ManualTests.java new file mode 100644 index 00000000..7ca68d36 --- /dev/null +++ b/src/test/java/lanat/test/manualTests/ManualTests.java @@ -0,0 +1,38 @@ +package lanat.test.manualTests; + +import lanat.ArgumentParser; +import lanat.CLInput; +import lanat.Command; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; + +public final class ManualTests { + @Test + public void main() { + String input = "--help --stdin --string hello --number 15 sub-command -ccc"; + + // write some stuff to stdin + System.setIn(new ByteArrayInputStream("hello world\ngoodbye".getBytes())); + + var parsed = new ArgumentParser(CommandTemplateExample.class) {{ + this.addCommand(new Command(CommandTemplateExample.MySubCommand.class) {{ + this.addCommand(new Command(CommandTemplateExample.MySubCommand.AnotherSubCommand.class)); + }}); + }} + .parse(CLInput.from(input)) + .printErrors() + .into(CommandTemplateExample.class); + + parsed.string + .ifPresentOrElse( + s -> System.out.println("String is present: " + s), + () -> System.out.println("String is not present") + ); + + System.out.println(parsed.number); + System.out.println(parsed.subCommand.counter); + System.out.println(parsed.subCommand.anotherSubCommand.counter); + System.out.println(parsed.stdin); + } +} \ No newline at end of file diff --git a/src/test/java/lanat/test/units/TestArgumentGroups.java b/src/test/java/lanat/test/units/TestArgumentGroups.java new file mode 100644 index 00000000..5b9b8ad8 --- /dev/null +++ b/src/test/java/lanat/test/units/TestArgumentGroups.java @@ -0,0 +1,33 @@ +package lanat.test.units; + +import lanat.Argument; +import lanat.ArgumentGroup; +import lanat.test.TestingParser; +import lanat.test.UnitTests; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestArgumentGroups extends UnitTests { + @Override + protected TestingParser setParser() { + final var parser = super.setParser(); + + parser.addGroup(new ArgumentGroup("group") {{ + this.setExclusive(true); + this.addArgument(Argument.createOfBoolType("group-arg")); + this.addArgument(Argument.createOfBoolType("group-arg2")); + }}); + + return parser; + } + + @Test + @DisplayName("Test exclusive group") + public void testExclusiveGroup() { + var parsedArgs = this.parser.parseGetValues("--group-arg --group-arg2"); + assertEquals(Boolean.TRUE, parsedArgs.get("group-arg").orElse(null)); + assertEquals(Boolean.FALSE, parsedArgs.get("group-arg2").orElse(null)); // group-arg2 should not be present + } +} \ No newline at end of file diff --git a/src/test/java/lanat/test/TestArgumentTypes.java b/src/test/java/lanat/test/units/TestArgumentTypes.java similarity index 58% rename from src/test/java/lanat/test/TestArgumentTypes.java rename to src/test/java/lanat/test/units/TestArgumentTypes.java index 81ccf332..5846d49f 100644 --- a/src/test/java/lanat/test/TestArgumentTypes.java +++ b/src/test/java/lanat/test/units/TestArgumentTypes.java @@ -1,7 +1,9 @@ -package lanat.test; +package lanat.test.units; import lanat.Argument; -import lanat.ArgumentType; +import lanat.argumentTypes.*; +import lanat.test.TestingParser; +import lanat.test.UnitTests; import org.junit.jupiter.api.Test; import java.io.File; @@ -15,33 +17,33 @@ private enum TestEnum { } @Override - public void setParser() { - this.parser = new TestingParser("TestArgumentTypes") {{ - this.addArgument(Argument.create("boolean", ArgumentType.BOOLEAN())); - this.addArgument(Argument.create(ArgumentType.COUNTER(), "counter", "c")); - this.addArgument(Argument.create("integer", ArgumentType.INTEGER())); - this.addArgument(Argument.create("float", ArgumentType.FLOAT())); - this.addArgument(Argument.create("string", ArgumentType.STRING())); - this.addArgument(Argument.create("multiple-strings", ArgumentType.STRINGS())); - this.addArgument(Argument.create("file", ArgumentType.FILE())); - this.addArgument(Argument.create("enum", ArgumentType.ENUM(TestEnum.TWO))); - this.addArgument(Argument.create("key-value", ArgumentType.KEY_VALUES(ArgumentType.INTEGER()))); - this.addArgument(Argument.create("int-range", ArgumentType.INTEGER_RANGE(3, 10))); - this.addArgument(Argument.create("try-parse", ArgumentType.TRY_PARSE(Double.class))); + protected TestingParser setParser() { + return new TestingParser("TestArgumentTypes") {{ + this.addArgument(Argument.createOfBoolType("boolean")); + this.addArgument(Argument.create(new CounterArgumentType(), "counter", "c")); + this.addArgument(Argument.create(new IntegerArgumentType(), "integer")); + this.addArgument(Argument.create(new FloatArgumentType(), "float")); + this.addArgument(Argument.create(new StringArgumentType(), "string")); + this.addArgument(Argument.create(new MultipleStringsArgumentType(), "multiple-strings")); + this.addArgument(Argument.create(new FileArgumentType(), "file")); + this.addArgument(Argument.create(new EnumArgumentType<>(TestEnum.TWO), "enum")); + this.addArgument(Argument.create(new KeyValuesArgumentType<>(new IntegerArgumentType()), "key-value")); + this.addArgument(Argument.create(new NumberRangeArgumentType<>(3, 10), "int-range")); + this.addArgument(Argument.create(new TryParseArgumentType<>(Double.class), "try-parse")); }}; } @Test public void testBoolean() { - assertEquals(Boolean.TRUE, this.parser.parseArgs("--boolean").get("boolean").get()); - assertEquals(Boolean.FALSE, this.parser.parseArgs("").get("boolean").get()); + assertEquals(Boolean.TRUE, this.parser.parseGetValues("--boolean").get("boolean").orElse(null)); + assertEquals(Boolean.FALSE, this.parser.parseGetValues("").get("boolean").orElse(null)); } @Test public void testCounter() { - assertEquals(0, this.parser.parseArgs("").get("counter").get()); - assertEquals(1, this.parser.parseArgs("-c").get("counter").get()); - assertEquals(4, this.parser.parseArgs("-cccc").get("counter").get()); + assertEquals(0, this.parser.parseGetValues("").get("counter").orElse(null)); + assertEquals(1, this.parser.parseGetValues("-c").get("counter").orElse(null)); + assertEquals(4, this.parser.parseGetValues("-cccc").get("counter").orElse(null)); } @Test @@ -84,7 +86,7 @@ public void testEnum() { assertEquals(TestEnum.ONE, this.parseArg("enum", "ONE")); assertEquals(TestEnum.TWO, this.parseArg("enum", "TWO")); assertEquals(TestEnum.THREE, this.parseArg("enum", "THREE")); - assertEquals(TestEnum.TWO, this.parser.parseArgs("").get("enum").get()); + assertEquals(TestEnum.TWO, this.parser.parseGetValues("").get("enum").orElse(null)); } @Test @@ -101,7 +103,7 @@ public void testKeyValue() { } @Test - public void testIntegerRange() { + public void testNumberRange() { assertEquals(4, this.parseArg("int-range", "4")); this.assertNotPresent("int-range"); assertNull(this.parseArg("int-range", "invalid")); diff --git a/src/test/java/lanat/test/TestErrors.java b/src/test/java/lanat/test/units/TestErrors.java similarity index 67% rename from src/test/java/lanat/test/TestErrors.java rename to src/test/java/lanat/test/units/TestErrors.java index 74e6e089..524c4f2e 100644 --- a/src/test/java/lanat/test/TestErrors.java +++ b/src/test/java/lanat/test/units/TestErrors.java @@ -1,6 +1,14 @@ -package lanat.test; - -import lanat.*; +package lanat.test.units; + +import lanat.Argument; +import lanat.CallbacksInvocationOption; +import lanat.Command; +import lanat.NamedWithDescription; +import lanat.argumentTypes.CounterArgumentType; +import lanat.argumentTypes.FloatArgumentType; +import lanat.argumentTypes.IntegerArgumentType; +import lanat.test.TestingParser; +import lanat.test.UnitTests; import lanat.utils.ErrorCallbacks; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; @@ -22,7 +30,7 @@ public void clear() { } private & NamedWithDescription> @NotNull T addCallbacks(@NotNull T obj) { - obj.setOnCorrectCallback(v -> this.correct.put(obj.getName(), v)); + obj.setOnOkCallback(v -> this.correct.put(obj.getName(), v)); obj.setOnErrorCallback(a -> this.invalid.put(obj.getName(), a)); return obj; } @@ -44,17 +52,17 @@ protected void assertNotPresent(@NotNull String name) { } @Override - public void setParser() { - this.parser = this.addCallbacks(new TestingParser("TestCallbacks") {{ + protected TestingParser setParser() { + return this.addCallbacks(new TestingParser("TestCallbacks") {{ this.setErrorCode(5); - this.addArgument(TestErrors.this.addCallbacks(Argument.create("bool-arg", ArgumentType.BOOLEAN()))); - this.addArgument(TestErrors.this.addCallbacks(Argument.create("int-arg", ArgumentType.INTEGER()))); - this.addArgument(TestErrors.this.addCallbacks(Argument.create("counter", ArgumentType.COUNTER()))); - this.addArgument(TestErrors.this.addCallbacks(Argument.create("float", ArgumentType.FLOAT()))); + this.addArgument(TestErrors.this.addCallbacks(Argument.createOfBoolType("bool-arg").build())); + this.addArgument(TestErrors.this.addCallbacks(Argument.create(new IntegerArgumentType(), "int-arg").build())); + this.addArgument(TestErrors.this.addCallbacks(Argument.create(new CounterArgumentType(), "counter").build())); + this.addArgument(TestErrors.this.addCallbacks(Argument.create(new FloatArgumentType(), "float").build())); - this.addSubCommand(TestErrors.this.addCallbacks(new Command("sub") {{ - this.addArgument(TestErrors.this.addCallbacks(Argument.create("sub-float", ArgumentType.FLOAT()))); + this.addCommand(TestErrors.this.addCallbacks(new Command("sub") {{ + this.addArgument(TestErrors.this.addCallbacks(Argument.create(new FloatArgumentType(), "sub-float").build())); this.setErrorCode(2); }})); }}); @@ -62,9 +70,9 @@ public void setParser() { @Test @DisplayName("Test the argument callbacks (onOk and onErr) (ArgumentCallbacksOption.NO_ERROR_IN_ARGUMENT)") - public void testArgumentCallbacks__NoErrorInArg() { - this.parser.invokeCallbacksWhen(CallbacksInvocationOption.NO_ERROR_IN_ARGUMENT); - this.parser.parseArgs("--bool-arg --int-arg foo --float 55.0 sub --sub-float bar"); + public void testArgumentCallbacks$NoErrorInArg() { + this.parser.setCallbackInvocationOption(CallbacksInvocationOption.NO_ERROR_IN_ARGUMENT); + this.parser.parseGetValues("--bool-arg --int-arg foo --float 55.0 sub --sub-float bar"); this.assertOk("bool-arg", true); this.assertErr("int-arg"); @@ -76,7 +84,7 @@ public void testArgumentCallbacks__NoErrorInArg() { @Test @DisplayName("Test the argument callbacks (onOk and onErr) (ArgumentCallbacksOption.(DEFAULT)))") public void testArgumentCallbacks() { - this.parser.parseArgs("--bool-arg --float foo sub --sub-float 5.23"); + this.parser.parseGetValues("--bool-arg --float foo sub --sub-float 5.23"); this.assertNotPresent("bool-arg"); this.assertNotPresent("counter"); @@ -87,7 +95,7 @@ public void testArgumentCallbacks() { @Test @DisplayName("Test the command callbacks (onOk and onErr)") public void testCommandCallbacks() { - this.parser.parseArgs("sub --sub-float bar"); + this.parser.parseGetValues("sub --sub-float bar"); this.assertErr("sub-float"); this.assertErr(this.parser.getName()); } @@ -95,7 +103,7 @@ public void testCommandCallbacks() { @Test @DisplayName("The error code must be the result of 5 | 2 = 7") public void testCommandsErrorCode() { - this.parser.parseArgs("sub --sub-float bar"); + this.parser.parseGetValues("sub --sub-float bar"); assertEquals(this.parser.getErrorCode(), 7); } } \ No newline at end of file diff --git a/src/test/java/lanat/test/TestHelpFormatting.java b/src/test/java/lanat/test/units/TestHelpFormatting.java similarity index 71% rename from src/test/java/lanat/test/TestHelpFormatting.java rename to src/test/java/lanat/test/units/TestHelpFormatting.java index a569d534..fcd4f010 100644 --- a/src/test/java/lanat/test/TestHelpFormatting.java +++ b/src/test/java/lanat/test/units/TestHelpFormatting.java @@ -1,11 +1,13 @@ -package lanat.test; +package lanat.test.units; import lanat.Argument; -import lanat.ArgumentType; +import lanat.argumentTypes.CounterArgumentType; import lanat.helpRepresentation.HelpFormatter; import lanat.helpRepresentation.LayoutItem; import lanat.helpRepresentation.descriptions.DescriptionFormatter; import lanat.helpRepresentation.descriptions.exceptions.InvalidRouteException; +import lanat.test.TestingParser; +import lanat.test.UnitTests; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,18 +18,8 @@ public class TestHelpFormatting extends UnitTests { private HelpFormatter helpFormatter; @Override - public void setParser() { - this.parser = new TestingParser( - "TestHelpFormatting", - "description of : ()" - ) {{ - this.addArgument(Argument.create("arg1", "a1") - .description("description of arg2: ()")); - this.addArgument(Argument.create("arg2", ArgumentType.COUNTER()) - .description("description of my type: () i am in the command ")); - }}; - - this.helpFormatter = new HelpFormatter(this.parser) { + protected TestingParser setParser() { + this.helpFormatter = new HelpFormatter() { @Override protected void initLayout() { this.setLayout( @@ -35,6 +27,17 @@ protected void initLayout() { ); } }; + + return new TestingParser( + "TestHelpFormatting", + "description of : ()" + ) + {{ + this.addArgument(Argument.createOfBoolType("arg1", "a1") + .withDescription("description of arg2: ()")); + this.addArgument(Argument.create(new CounterArgumentType(), "arg2") + .withDescription("description of my type: () i am in the command ")); + }}; } @Test @@ -43,7 +46,7 @@ public void testHelpFormatting() { assertEquals( "description of --arg1/a1: (description of arg2: (description of my type: " + "(Counts the number of times this argument is used.) i am in the command TestHelpFormatting))", - this.helpFormatter.toString() + this.helpFormatter.generate(this.parser) ); } diff --git a/src/test/java/lanat/test/units/TestMisc.java b/src/test/java/lanat/test/units/TestMisc.java new file mode 100644 index 00000000..e2c550f8 --- /dev/null +++ b/src/test/java/lanat/test/units/TestMisc.java @@ -0,0 +1,38 @@ +package lanat.test.units; + +import lanat.CLInput; +import lanat.exceptions.ArgumentAlreadyExistsException; +import lanat.test.UnitTests; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TestMisc extends UnitTests { + @Test + @DisplayName("check duplicate names in arguments") + public void testDuplicateNames() { + assertThrows( + ArgumentAlreadyExistsException.class, + () -> this.parser.getArgument("what").addNames("a"), + "check duplicate names after initialization" + ); + } + + @Test + @DisplayName("check error codes are correct") + public void testErrorCodes() { + // test first command failing (its error code is 0b0100) + assertEquals(0b0100, this.parser.parse(CLInput.from("")).getErrorCode()); + + // test sub-command failing (its error code is 0b0010) + assertEquals(0b0110, this.parser.parse(CLInput.from("subCommand -s")).getErrorCode()); + + // test innermost sub-command failing (its error code is 0b0001) + assertEquals(0b0111, this.parser.parse(CLInput.from("subCommand another")).getErrorCode()); + + // test sub-command2 failing (its error code is 0b1000) + assertEquals(0b1100, this.parser.parse(CLInput.from("subCommand2 hello")).getErrorCode()); + } +} diff --git a/src/test/java/lanat/test/units/TestParsedValues.java b/src/test/java/lanat/test/units/TestParsedValues.java new file mode 100644 index 00000000..8caa50ba --- /dev/null +++ b/src/test/java/lanat/test/units/TestParsedValues.java @@ -0,0 +1,74 @@ +package lanat.test.units; + +import lanat.ParsedArgumentsRoot; +import lanat.exceptions.ArgumentNotFoundException; +import lanat.test.UnitTests; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestParsedValues extends UnitTests { + private ParsedArgumentsRoot parseArgs(String args) { + return this.parser.parseGetValues(args); + } + + @Test + @DisplayName("Test the get() method") + public void testGetSimple() { + assertEquals("(hello), (world)", this.parseArgs("--what hello world").get("what").orElse(null)); + } + + @Test + @DisplayName("Exception thrown when querying an invalid argument") + public void testUnknownArg() { + assertThrows( + ArgumentNotFoundException.class, + () -> this.parseArgs("--what hello world").get("not-there") + ); + } + + @Test + @DisplayName("Test querying parsed values from arguments in Sub-Commands") + public void testNestedArguments() { + var parsedArgs = this.parseArgs("smth subCommand -cccc another 56"); + assertEquals(4, parsedArgs.get("subCommand.c").orElse(null)); + assertEquals(4, parsedArgs.get("subCommand", "c").orElse(null)); + + assertEquals(56, parsedArgs.get("subCommand.another.number").orElse(null)); + assertEquals(56, parsedArgs.get("subCommand", "another", "number").orElse(null)); + } + + @Test + @DisplayName("Test values present or not") + public void testDefinedCallbacks() { + var parsedArgs = this.parseArgs("smth subCommand -cccc"); + final byte[] called = { 0 }; + + parsedArgs.get("subCommand.c").ifPresent(v -> { + assertEquals(4, v); + called[0]++; + }); + + parsedArgs.get("subCommand.another.number").ifPresentOrElse((v) -> {}, () -> called[0]++); + + assertEquals(2, called[0]); + } + + @Test + @DisplayName("Test the forward value") + public void testForwardValue() { + { + Optional parsedArgs = this.parseArgs("foo -- hello world").getForwardValue(); + assertTrue(parsedArgs.isPresent()); + assertEquals("hello world", parsedArgs.get()); + } + + { + Optional parsedArgs = this.parseArgs("foo").getForwardValue(); + assertFalse(parsedArgs.isPresent()); + } + } +} \ No newline at end of file diff --git a/src/test/java/lanat/test/TestTerminalOutput.java b/src/test/java/lanat/test/units/TestTerminalOutput.java similarity index 96% rename from src/test/java/lanat/test/TestTerminalOutput.java rename to src/test/java/lanat/test/units/TestTerminalOutput.java index a7b316d5..efc3ec7f 100644 --- a/src/test/java/lanat/test/TestTerminalOutput.java +++ b/src/test/java/lanat/test/units/TestTerminalOutput.java @@ -1,5 +1,6 @@ -package lanat.test; +package lanat.test.units; +import lanat.test.UnitTests; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -7,7 +8,7 @@ public class TestTerminalOutput extends UnitTests { private void assertErrorOutput(String args, String expected) { - final var errors = this.parser.parseArgsExpectError(args); + final var errors = this.parser.parseGetErrors(args); // remove all the decorations to not make the tests a pain to write assertEquals( expected, @@ -84,7 +85,7 @@ public void testInvalidArgumentTypeValue() { this.assertErrorOutput("foo subCommand another bar", """ ERROR Testing foo subCommand another bar - Invalid integer value: 'bar'."""); + Invalid Integer value: 'bar'."""); } @Test diff --git a/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java b/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java new file mode 100644 index 00000000..8d8a9b50 --- /dev/null +++ b/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java @@ -0,0 +1,86 @@ +package lanat.test.units.commandTemplates; + +import lanat.Argument; +import lanat.Command; +import lanat.CommandTemplate; +import lanat.argumentTypes.BooleanArgumentType; +import lanat.argumentTypes.FloatArgumentType; +import lanat.argumentTypes.IntegerArgumentType; +import lanat.argumentTypes.StringArgumentType; + +import java.util.Optional; + +public class CmdTemplates { + @Command.Define(names = "cmd1") + public static class CmdTemplate1 extends CommandTemplate { + @Argument.Define(argType = IntegerArgumentType.class) + public Integer number; + + @Argument.Define(argType = StringArgumentType.class) + public String text; + + @Argument.Define(names = { "name1", "f" }, argType = BooleanArgumentType.class) + public boolean flag; + + @Argument.Define(argType = IntegerArgumentType.class) + public Optional numberParsedArgValue = Optional.of(0); + + + @CommandAccessor + public CmdTemplate1_1 cmd2; + + @Command.Define(names = "cmd1-1") + public static class CmdTemplate1_1 extends CommandTemplate { + @Argument.Define(argType = FloatArgumentType.class) + public Float number; + + @Argument.Define(argType = IntegerArgumentType.class) + public Optional number2; + } + } + + @Command.Define(names = "cmd2") + public static class CmdTemplate2 extends CommandTemplate { + @Command.Define + public static class CmdTemplate2_1 extends CommandTemplate { } + } + + @Command.Define + public static class CmdTemplate3 extends CommandTemplate { + @Argument.Define(argType = IntegerArgumentType.class, positional = true) + public int number; + + @CommandAccessor + public CmdTemplate3_1 cmd2; + + @Command.Define(names = "cmd3-1") + public static class CmdTemplate3_1 extends CommandTemplate { + @Argument.Define(argType = IntegerArgumentType.class, positional = true) + public int number; + + @CommandAccessor + public CmdTemplate3_1_1 cmd3; + + @Command.Define(names = "cmd3-1-1") + public static class CmdTemplate3_1_1 extends CommandTemplate { + @Argument.Define(argType = IntegerArgumentType.class, positional = true) + public int number; + } + } + } + + @Command.Define + public static class CmdTemplate4 extends CommandTemplate { + @Argument.Define + public int number; + + @Argument.Define + public String text; + + @Argument.Define + public boolean flag; + + @Argument.Define + public Double number2; + } +} diff --git a/src/test/java/lanat/test/units/commandTemplates/TestCommandTemplates.java b/src/test/java/lanat/test/units/commandTemplates/TestCommandTemplates.java new file mode 100644 index 00000000..c90f21e8 --- /dev/null +++ b/src/test/java/lanat/test/units/commandTemplates/TestCommandTemplates.java @@ -0,0 +1,80 @@ +package lanat.test.units.commandTemplates; + +import lanat.CLInput; +import lanat.Command; +import lanat.exceptions.ArgumentNotFoundException; +import lanat.test.TestingParser; +import lanat.test.UnitTests; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestCommandTemplates extends UnitTests { + @Override + protected TestingParser setParser() { + return new TestingParser(CmdTemplates.CmdTemplate1.class) {{ + this.addCommand(new Command(CmdTemplates.CmdTemplate1.CmdTemplate1_1.class)); + }}; + } + + @Test + @DisplayName("assert CmdTemplate1 instance is created and arguments are set") + public void testCmdTemplate1() { + assertEquals(56, this.parseArg("number", "56")); + assertEquals("hello", this.parseArg("text", "hello")); + assertTrue(this.parseArg("name1", "")); + assertTrue(this.parseArg("f", "")); + assertThrows( + ArgumentNotFoundException.class, + () -> this.parseArg("flag", "") + ); + } + + @Test + @DisplayName("test into method") + public void testInto() { + final var result = this.parser.parse(CLInput.from("--number 56 --text hello -f")) + .into(CmdTemplates.CmdTemplate1.class); + + assertTrue(result.flag); + assertEquals(56, result.number); + assertEquals("hello", result.text); + assertNull(result.cmd2.number); + } + + @Test + @DisplayName("test ParsedArgumentValue wrapper") + public void testParsedArgumentValue() { + final var result = this.parser.parse(CLInput.from("cmd1-1 --number2 14")) + .into(CmdTemplates.CmdTemplate1.class); + + assertTrue(result.cmd2.number2.isPresent()); + assertEquals(14, result.cmd2.number2.get()); + + assertTrue( + this.parser.parse(CLInput.from("")) + .into(CmdTemplates.CmdTemplate1.class).cmd2.number2.isEmpty() + ); + } + + @Test + @DisplayName("test default values for arguments") + public void testDefaultValues() { + { + final var result = this.parser.parse(CLInput.from("")) + .into(CmdTemplates.CmdTemplate1.class); + + assertTrue(result.numberParsedArgValue.isPresent()); + assertEquals(0, result.numberParsedArgValue.get()); + } + + { + final var result = this.parser.parse(CLInput.from("--numberParsedArgValue 56")) + .into(CmdTemplates.CmdTemplate1.class); + + assertTrue(result.numberParsedArgValue.isPresent()); + assertEquals(56, result.numberParsedArgValue.get()); + } + } +} diff --git a/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java b/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java new file mode 100644 index 00000000..e3dd43de --- /dev/null +++ b/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java @@ -0,0 +1,71 @@ +package lanat.test.units.commandTemplates; + +import lanat.ArgumentParser; +import lanat.CLInput; +import lanat.CommandTemplate; +import lanat.argumentTypes.BooleanArgumentType; +import lanat.argumentTypes.DoubleArgumentType; +import lanat.argumentTypes.IntegerArgumentType; +import lanat.argumentTypes.StringArgumentType; +import lanat.exceptions.CommandTemplateException; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestFromInto { + private static @NotNull T parseFromInto( + @NotNull Class templateClass, + @NotNull CLInput input + ) { + return ArgumentParser.parseFromInto(templateClass, input, ArgumentParser.AfterParseOptions::printErrors); + } + + @Test + @DisplayName("test parseFromInto method") + public void testParseFromInto() { + final var result = TestFromInto.parseFromInto( + CmdTemplates.CmdTemplate1.class, CLInput.from("--number 56 --text hello -f cmd1-1 --number 54.0") + ); + + assertTrue(result.flag); + assertEquals(56, result.number); + assertEquals("hello", result.text); + assertEquals(54.0f, result.cmd2.number); + } + + @Test + @DisplayName("test command template with sub-command but no accessor") + public void testCmdTemplate2() { + assertThrows(CommandTemplateException.class, () -> + TestFromInto.parseFromInto( + CmdTemplates.CmdTemplate2.class, CLInput.from("") + ) + ); + } + + @Test + @DisplayName("test nested commands") + public void testNestedCommands() { + final var result = TestFromInto.parseFromInto( + CmdTemplates.CmdTemplate3.class, CLInput.from("56 cmd3-1 54 cmd3-1-1 52") + ); + + assertEquals(56, result.number); + assertEquals(54, result.cmd2.number); + assertEquals(52, result.cmd2.cmd3.number); + } + + + @Test + @DisplayName("test type inference for fields argument types") + public void testTypeInference() { + final var result = ArgumentParser.from(CmdTemplates.CmdTemplate4.class); + + assertTrue(result.getArgument("number").argType instanceof IntegerArgumentType); + assertTrue(result.getArgument("text").argType instanceof StringArgumentType); + assertTrue(result.getArgument("flag").argType instanceof BooleanArgumentType); + assertTrue(result.getArgument("number2").argType instanceof DoubleArgumentType); + } +} \ No newline at end of file diff --git a/src/test/java/module-info.java b/src/test/java/module-info.java index 58cfe5e6..8f9b6c4f 100644 --- a/src/test/java/module-info.java +++ b/src/test/java/module-info.java @@ -2,7 +2,9 @@ requires org.junit.jupiter.api; requires lanat; requires org.jetbrains.annotations; - requires fade.mirror; - exports lanat.test to org.junit.platform.commons; + exports lanat.test to org.junit.platform.commons, lanat; + exports lanat.test.manualTests to org.junit.platform.commons, lanat; + exports lanat.test.units to lanat, org.junit.platform.commons; + exports lanat.test.units.commandTemplates to lanat, org.junit.platform.commons; } \ No newline at end of file