From 9cb519c26f04577a3336651b9a591c35b2aebd41 Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Mon, 25 Sep 2023 21:19:52 +0200 Subject: [PATCH 01/15] fix: slightly optimized tokenizer --- src/main/java/lanat/parsing/Parser.java | 6 +++++- src/main/java/lanat/parsing/Tokenizer.java | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/lanat/parsing/Parser.java b/src/main/java/lanat/parsing/Parser.java index d7a0a7f8..ea05bc96 100644 --- a/src/main/java/lanat/parsing/Parser.java +++ b/src/main/java/lanat/parsing/Parser.java @@ -81,8 +81,12 @@ public void setTokens(@NotNull List<@NotNull Token> tokens) { this.tokens = tokens; } + /** + * Parses the tokens that have been set. Delegates parsing of argument values to the {@link ArgumentType} of the + * argument that is being parsed. + */ public void parseTokens() { - assert this.tokens != null : "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."; // number of positional arguments that have been parsed. diff --git a/src/main/java/lanat/parsing/Tokenizer.java b/src/main/java/lanat/parsing/Tokenizer.java index 0e4726e8..0397ac40 100644 --- a/src/main/java/lanat/parsing/Tokenizer.java +++ b/src/main/java/lanat/parsing/Tokenizer.java @@ -54,10 +54,16 @@ private void setInputString(@NotNull String inputString) { * {@link Tokenizer#getFinalTokens()} */ public void tokenize(@NotNull String input) { - assert !this.hasFinished : "Tokenizer has already finished tokenizing"; + assert !this.hasFinished : "Tokenizer has already finished tokenizing."; this.setInputString(input); + // nothing to tokenize. Just finish + if (input.isEmpty()) { + this.hasFinished = true; + return; + } + char currentStringChar = 0; // the character that opened the string TokenizeError.TokenizeErrorType errorType = null; From 0459e940ff8c1f57b6d3e283a6dbf015bacdf26c Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Mon, 25 Sep 2023 21:38:58 +0200 Subject: [PATCH 02/15] fix: slight changes to access modifiers --- src/main/java/lanat/ArgumentParser.java | 2 +- src/main/java/lanat/Command.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java index 14d0368c..21d4073f 100644 --- a/src/main/java/lanat/ArgumentParser.java +++ b/src/main/java/lanat/ArgumentParser.java @@ -191,7 +191,7 @@ public static ArgumentParser from(@NotNull Class temp return new ParsedArgumentsRoot( this, this.getParser().getParsedArgumentsHashMap(), - this.subCommands.stream().map(Command::getParsedArguments).toList(), + this.getCommands().stream().map(Command::getParsedArguments).toList(), this.getForwardValue() ); } diff --git a/src/main/java/lanat/Command.java b/src/main/java/lanat/Command.java index da9611e4..c6665ad9 100644 --- a/src/main/java/lanat/Command.java +++ b/src/main/java/lanat/Command.java @@ -43,10 +43,10 @@ public class Command { private final @NotNull List<@NotNull String> names = new ArrayList<>(); private @Nullable String description; - final @NotNull ArrayList<@NotNull Argument> arguments = new ArrayList<>(); - final @NotNull ArrayList<@NotNull Command> subCommands = new ArrayList<>(); + private final @NotNull ArrayList<@NotNull Argument> arguments = new ArrayList<>(); + private final @NotNull ArrayList<@NotNull Command> subCommands = new ArrayList<>(); private Command parentCommand; - final @NotNull ArrayList<@NotNull ArgumentGroup> argumentGroups = new ArrayList<>(); + private final @NotNull ArrayList<@NotNull ArgumentGroup> argumentGroups = new ArrayList<>(); private final @NotNull ModifyRecord<@NotNull TupleCharacter> tupleChars = ModifyRecord.of(TupleCharacter.SQUARE_BRACKETS); private final @NotNull ModifyRecord<@NotNull Integer> errorCode = ModifyRecord.of(1); From 9b24df3a8f78e77a5e50930674e9f5509b678c69 Mon Sep 17 00:00:00 2001 From: David L <48654552+DarviL82@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:28:54 +0200 Subject: [PATCH 03/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d470a1a..1a83a354 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The package is currently only available on GitHub Packages. implementation("darvil:lanat") ``` - Note that you may need to explicitly specify the version of the package you want to use. (e.g. `darvil:lanat:0.0.1`) + Note that you may need to explicitly specify the version of the package you want to use. (e.g. `darvil:lanat:x.x.x`) This information is available at the [GitHub Packages documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package). From d1b688f2b47381322905088bfb8f5fbdd94fd65b Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Fri, 6 Oct 2023 20:39:13 +0200 Subject: [PATCH 04/15] fix: slighty optimized .equals methods --- src/main/java/lanat/Argument.java | 1 + src/main/java/lanat/ArgumentGroup.java | 5 +++-- src/main/java/lanat/Command.java | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/lanat/Argument.java b/src/main/java/lanat/Argument.java index ded72661..a01876c0 100644 --- a/src/main/java/lanat/Argument.java +++ b/src/main/java/lanat/Argument.java @@ -597,6 +597,7 @@ void invokeCallbacks(@Nullable Object okValue) { */ @Override public boolean equals(@NotNull Object obj) { + if (obj == this) return true; if (obj instanceof Argument arg) return UtlMisc.equalsByNamesAndParentCmd(this, arg); return false; diff --git a/src/main/java/lanat/ArgumentGroup.java b/src/main/java/lanat/ArgumentGroup.java index fcc230a8..9053157e 100644 --- a/src/main/java/lanat/ArgumentGroup.java +++ b/src/main/java/lanat/ArgumentGroup.java @@ -75,8 +75,8 @@ public class ArgumentGroup * each one added to this group is because at parsing, we might need to know which arguments were used in this * group. *

- * Sure, we could just use {@link Command#arguments}, but that would mean that we would have to iterate through all - * the arguments in there for filtering ours, which is probably worse. + * Sure, we could just use {@link Command#getArguments()}, but that would mean that we would have + * to iterate through all the arguments in there for filtering ours, which is probably worse. */ private final @NotNull List<@NotNull Argument> arguments = new ArrayList<>(); @@ -274,6 +274,7 @@ public ArgumentGroup getParent() { @Override public boolean equals(@NotNull Object obj) { + if (obj == this) return true; if (obj instanceof ArgumentGroup group) return this.parentCommand == group.parentCommand && this.name.equals(group.name); return false; diff --git a/src/main/java/lanat/Command.java b/src/main/java/lanat/Command.java index c6665ad9..6c752dcf 100644 --- a/src/main/java/lanat/Command.java +++ b/src/main/java/lanat/Command.java @@ -412,6 +412,7 @@ void passPropertiesToChildren() { */ @Override public boolean equals(@NotNull Object obj) { + if (obj == this) return true; if (obj instanceof Command cmd) return UtlMisc.equalsByNamesAndParentCmd(this, cmd); return false; From a60268be003e90698641ff7629d25312f1517404 Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Tue, 17 Oct 2023 21:27:45 +0200 Subject: [PATCH 05/15] update version number to 0.0.3 feat: add `needsToExist` option to FileArgumentType docs: add more javadocs fix: FileArgumentType not behaving as expected. fix: incorrect tests for FileArgumentType --- build.gradle.kts | 2 +- src/main/java/lanat/ArgumentType.java | 2 +- .../lanat/argumentTypes/EnumArgumentType.java | 4 ++++ .../lanat/argumentTypes/FileArgumentType.java | 22 ++++++++++++++----- .../NumberRangeArgumentType.java | 5 +++++ src/test/java/lanat/test/TestingParser.java | 2 +- .../lanat/test/units/TestArgumentTypes.java | 5 ++--- 7 files changed, 30 insertions(+), 12 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b631a142..15dcd597 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "darvil" -version = "0.0.2" +version = "0.0.3" description = "Command line argument parser" dependencies { diff --git a/src/main/java/lanat/ArgumentType.java b/src/main/java/lanat/ArgumentType.java index 2f05f4d6..c5bfcb04 100644 --- a/src/main/java/lanat/ArgumentType.java +++ b/src/main/java/lanat/ArgumentType.java @@ -92,7 +92,7 @@ public ArgumentType(@NotNull T initialValue) { } /** - * Constructs a new argument type with no initial value. + * Constructs a new argument type. */ public ArgumentType() { if (this.getRequiredUsageCount().start() == 0) { diff --git a/src/main/java/lanat/argumentTypes/EnumArgumentType.java b/src/main/java/lanat/argumentTypes/EnumArgumentType.java index 31e6d793..cb90d941 100644 --- a/src/main/java/lanat/argumentTypes/EnumArgumentType.java +++ b/src/main/java/lanat/argumentTypes/EnumArgumentType.java @@ -20,6 +20,10 @@ public class EnumArgumentType> extends ArgumentType { private final @NotNull T @NotNull [] values; + /** + * Creates a new enum argument type. + * @param defaultValue The default value of the enum type. This is also used to infer the type of the enum. + */ public EnumArgumentType(@NotNull T defaultValue) { super(defaultValue); this.values = defaultValue.getDeclaringClass().getEnumConstants(); diff --git a/src/main/java/lanat/argumentTypes/FileArgumentType.java b/src/main/java/lanat/argumentTypes/FileArgumentType.java index afcab7c2..28179b8a 100644 --- a/src/main/java/lanat/argumentTypes/FileArgumentType.java +++ b/src/main/java/lanat/argumentTypes/FileArgumentType.java @@ -7,18 +7,28 @@ import java.io.File; /** - * 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. + * An argument type that takes a file path and returns a {@link File} instance representing it. */ public class FileArgumentType extends ArgumentType { + private final boolean needsToExist; + + /** + * Creates a new file argument type. + * @param needsToExist whether the file needs to exist or not. If set to false, the file instance will be created + * even it doesn't exist. + */ + public FileArgumentType(boolean needsToExist) { + this.needsToExist = needsToExist; + } + @Override public File parseValues(@NotNull String @NotNull [] args) { - try { - return new File(args[0]); - } catch (Exception e) { - this.addError("File not found: '" + args[0] + "'."); + File file = new File(args[0]); + if (this.needsToExist && !file.exists()) { + this.addError("File does not exist."); return null; } + return file; } @Override diff --git a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java index 78cca4ee..0cac8054 100644 --- a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java +++ b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java @@ -17,6 +17,11 @@ public class NumberRangeArgumentType> extends A private final ArgumentType argumentType; private final T min, max; + /** + * Creates a new number range argument type. + * @param min The minimum value. + * @param max The maximum value. + */ @SuppressWarnings("unchecked") public NumberRangeArgumentType(@NotNull T min, @NotNull T max) { if (min.compareTo(max) > 0) { diff --git a/src/test/java/lanat/test/TestingParser.java b/src/test/java/lanat/test/TestingParser.java index c0f70263..eb5a3cfb 100644 --- a/src/test/java/lanat/test/TestingParser.java +++ b/src/test/java/lanat/test/TestingParser.java @@ -28,7 +28,7 @@ public List parseGetErrors(String args) { } public @NotNull ParsedArgumentsRoot parseGetValues(@NotNull String args) { - var res = this.parse(CLInput.from(args)).getParsedArguments(); + var res = this.parse(CLInput.from(args)).printErrors().getParsedArguments(); assertNotNull(res, "The result of the parsing was null (Arguments have failed)"); return res; } diff --git a/src/test/java/lanat/test/units/TestArgumentTypes.java b/src/test/java/lanat/test/units/TestArgumentTypes.java index 5846d49f..7b4a1873 100644 --- a/src/test/java/lanat/test/units/TestArgumentTypes.java +++ b/src/test/java/lanat/test/units/TestArgumentTypes.java @@ -25,7 +25,7 @@ protected TestingParser setParser() { 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 FileArgumentType(true), "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")); @@ -77,8 +77,7 @@ public void testStrings() { @Test public void testFile() { - assertEquals("hello.txt", this.parseArg("file", "hello.txt").getName()); - this.assertNotPresent("file"); + assertNull(this.parseArg("file", "hello.txt")); } @Test From 8194d6a25ab50e94a1a45ef8e16d055d3d0c93cd Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Wed, 18 Oct 2023 03:04:09 +0200 Subject: [PATCH 06/15] feat: add File and Strin[] to the types that can be inferred. feat: registering argument types for inferring now requires specifying a supplier lambda instead of just the class. --- src/main/java/lanat/ArgumentBuilder.java | 7 +-- src/main/java/lanat/ArgumentType.java | 39 -------------- src/main/java/lanat/ArgumentTypeInfer.java | 52 +++++++++++++++++++ src/main/java/lanat/CommandTemplate.java | 2 +- .../NumberRangeArgumentType.java | 6 +-- 5 files changed, 58 insertions(+), 48 deletions(-) create mode 100644 src/main/java/lanat/ArgumentTypeInfer.java diff --git a/src/main/java/lanat/ArgumentBuilder.java b/src/main/java/lanat/ArgumentBuilder.java index 09ee1f15..a2839410 100644 --- a/src/main/java/lanat/ArgumentBuilder.java +++ b/src/main/java/lanat/ArgumentBuilder.java @@ -47,11 +47,8 @@ public class ArgumentBuilder, TInner> { 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); + // try to infer the type from the field type. If it can't be inferred, return null + return ArgumentTypeInfer.get(field.getType()); } /** diff --git a/src/main/java/lanat/ArgumentType.java b/src/main/java/lanat/ArgumentType.java index c5bfcb04..49e9c910 100644 --- a/src/main/java/lanat/ArgumentType.java +++ b/src/main/java/lanat/ArgumentType.java @@ -9,7 +9,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.HashMap; import java.util.function.Consumer; /** @@ -78,9 +77,6 @@ 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. @@ -298,39 +294,4 @@ public void resetState() { public @Nullable ArgumentType getParent() { return this.parentArgType; } - - - /** - * 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); - } - } - - /** - * 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/ArgumentTypeInfer.java b/src/main/java/lanat/ArgumentTypeInfer.java new file mode 100644 index 00000000..40f207fd --- /dev/null +++ b/src/main/java/lanat/ArgumentTypeInfer.java @@ -0,0 +1,52 @@ +package lanat; + +import lanat.argumentTypes.*; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.HashMap; +import java.util.function.Supplier; + +public class ArgumentTypeInfer { + /** + * Mapping of types to their corresponding argument types. Used for inferring. + * Argument types are stored as suppliers so that they can be instantiated when needed. + * */ + private static final HashMap, Supplier>> INFER_ARGUMENT_TYPES_MAP = new HashMap<>(); + + /** + * 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 register(@NotNull Supplier> type, @NotNull Class... infer) { + for (Class clazz : infer) { + ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.put(clazz, 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 ArgumentType get(@NotNull Class clazz) { + return ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.get(clazz).get(); + } + + // add some default argument types. + static { + // we need to also specify the primitives... wish there was a better way to do this. + register(StringArgumentType::new, String.class); + register(MultipleStringsArgumentType::new, String[].class); + register(IntegerArgumentType::new, int.class, Integer.class); + register(BooleanArgumentType::new, boolean.class, Boolean.class); + register(FloatArgumentType::new, float.class, Float.class); + register(DoubleArgumentType::new, double.class, Double.class); + register(LongArgumentType::new, long.class, Long.class); + register(ShortArgumentType::new, short.class, Short.class); + register(ByteArgumentType::new, byte.class, Byte.class); + register(() -> new FileArgumentType(false), File.class); + } +} diff --git a/src/main/java/lanat/CommandTemplate.java b/src/main/java/lanat/CommandTemplate.java index 52f18967..2a79e62e 100644 --- a/src/main/java/lanat/CommandTemplate.java +++ b/src/main/java/lanat/CommandTemplate.java @@ -51,7 +51,7 @@ *

* 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. + * {@link String}, {@link Integer}, {@link java.io.File}, etc. *

* * Example: diff --git a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java index 0cac8054..907395a9 100644 --- a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java +++ b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java @@ -1,7 +1,7 @@ package lanat.argumentTypes; import lanat.ArgumentType; -import lanat.utils.UtlReflection; +import lanat.ArgumentTypeInfer; import lanat.utils.displayFormatter.Color; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; @@ -28,13 +28,13 @@ public NumberRangeArgumentType(@NotNull T min, @NotNull T max) { throw new IllegalArgumentException("min must be less than or equal to max"); } - final var typeInferred = ArgumentType.getTypeInfer(min.getClass()); + final var typeInferred = ArgumentTypeInfer.get(min.getClass()); if (typeInferred == null) { throw new IllegalArgumentException("Unsupported type: " + min.getClass().getName()); } - this.argumentType = (ArgumentType)UtlReflection.instantiate(typeInferred); + this.argumentType = (ArgumentType)typeInferred; this.registerSubType(this.argumentType); this.min = min; From 42083e3fe02a7553bef68950290f0715f24e52b5 Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Wed, 18 Oct 2023 14:59:14 +0200 Subject: [PATCH 07/15] feat: replace TupleArgumentType functionality so that now it is basically a mapper of strings to what a subtype decides to parse feat: add MultipleNumbersArgumentType feat: refactor MultipleStringsArgumentType to inherit new TupleArgumentType feat: array types may now be inferred from Command Templates automatically as well. This includes all main number types and String. fix: ArgumentTypeInfer#register allowing to pass no Classes, essentially doing nothing. fix: ArgumentTypeInfer#register allowing to overwrite previous definitions. add: test for MultipleNumbersArgumentType --- src/main/java/lanat/ArgumentBuilder.java | 7 ++- src/main/java/lanat/ArgumentTypeInfer.java | 63 +++++++++++++++---- .../MultipleNumbersArgumentType.java | 28 +++++++++ .../MultipleStringsArgumentType.java | 19 +++--- .../NumberRangeArgumentType.java | 9 +-- .../argumentTypes/TupleArgumentType.java | 36 +++++++++-- .../ArgumentTypeInferException.java | 7 +++ src/main/java/lanat/utils/UtlReflection.java | 1 - src/test/java/lanat/test/UnitTests.java | 8 +-- .../lanat/test/units/TestArgumentTypes.java | 13 +++- 10 files changed, 147 insertions(+), 44 deletions(-) create mode 100644 src/main/java/lanat/argumentTypes/MultipleNumbersArgumentType.java create mode 100644 src/main/java/lanat/exceptions/ArgumentTypeInferException.java diff --git a/src/main/java/lanat/ArgumentBuilder.java b/src/main/java/lanat/ArgumentBuilder.java index a2839410..92c8142b 100644 --- a/src/main/java/lanat/ArgumentBuilder.java +++ b/src/main/java/lanat/ArgumentBuilder.java @@ -1,6 +1,7 @@ package lanat; import lanat.argumentTypes.DummyArgumentType; +import lanat.exceptions.ArgumentTypeInferException; import lanat.utils.UtlReflection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -48,7 +49,11 @@ public class ArgumentBuilder, TInner> { return UtlReflection.instantiate(annotation.argType()); // try to infer the type from the field type. If it can't be inferred, return null - return ArgumentTypeInfer.get(field.getType()); + try { + return ArgumentTypeInfer.get(field.getType()); + } catch (ArgumentTypeInferException e) { + return null; + } } /** diff --git a/src/main/java/lanat/ArgumentTypeInfer.java b/src/main/java/lanat/ArgumentTypeInfer.java index 40f207fd..5c1efd66 100644 --- a/src/main/java/lanat/ArgumentTypeInfer.java +++ b/src/main/java/lanat/ArgumentTypeInfer.java @@ -1,18 +1,22 @@ package lanat; import lanat.argumentTypes.*; +import lanat.exceptions.ArgumentTypeInferException; +import lanat.utils.Range; import org.jetbrains.annotations.NotNull; import java.io.File; +import java.util.Arrays; import java.util.HashMap; import java.util.function.Supplier; public class ArgumentTypeInfer { /** * Mapping of types to their corresponding argument types. Used for inferring. - * Argument types are stored as suppliers so that they can be instantiated when needed. + * Argument types are stored as suppliers so that we have no shared references. * */ private static final HashMap, Supplier>> INFER_ARGUMENT_TYPES_MAP = new HashMap<>(); + public static final Range DEFAULT_TYPE_RANGE = Range.AT_LEAST_ONE; /** * Registers an argument type to be inferred for the specified type/s. @@ -20,33 +24,66 @@ public class ArgumentTypeInfer { * @param infer The types to infer the argument type for. */ public static void register(@NotNull Supplier> type, @NotNull Class... infer) { + if (infer.length == 0) + throw new IllegalArgumentException("Must specify at least one type to infer the argument type for."); + for (Class clazz : infer) { + if (ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.containsKey(clazz)) + throw new IllegalArgumentException("Argument type already registered for type: " + clazz.getName()); + ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.put(clazz, type); } } /** - * Returns the argument type that should be inferred for the specified type. + * Returns a new argument type instance 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. + * @return The argument type that should be inferred for the specified type. + * @throws ArgumentTypeInferException If no argument type is found for the specified type. */ public static ArgumentType get(@NotNull Class clazz) { - return ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.get(clazz).get(); + var result = ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.get(clazz); + + if (result == null) + throw new ArgumentTypeInferException(clazz); + + return result.get(); + } + + /** + * Registers a numeric argument type with the specified tuple type as well. + * @param type The type of the numeric argument type. + * @param array The default value of the numeric argument type. + * @param infer The non-array types to infer the argument type for. + * @param The type of the numeric type. + * @param The type of the tuple argument type. + */ + private static > + void registerNumericWithTuple(@NotNull Supplier type, Ti[] array, @NotNull Class... infer) { + ArgumentTypeInfer.register(type, infer); + ArgumentTypeInfer.register( + () -> new MultipleNumbersArgumentType<>(DEFAULT_TYPE_RANGE, array), + Arrays.stream(infer) + .map(Class::arrayType) + .toArray(Class[]::new) + ); } // add some default argument types. static { - // we need to also specify the primitives... wish there was a better way to do this. register(StringArgumentType::new, String.class); - register(MultipleStringsArgumentType::new, String[].class); - register(IntegerArgumentType::new, int.class, Integer.class); + register(() -> new MultipleStringsArgumentType(DEFAULT_TYPE_RANGE), String[].class); + register(BooleanArgumentType::new, boolean.class, Boolean.class); - register(FloatArgumentType::new, float.class, Float.class); - register(DoubleArgumentType::new, double.class, Double.class); - register(LongArgumentType::new, long.class, Long.class); - register(ShortArgumentType::new, short.class, Short.class); - register(ByteArgumentType::new, byte.class, Byte.class); + register(() -> new FileArgumentType(false), File.class); + + // we need to specify the primitives as well... wish there was a better way to do this. + registerNumericWithTuple(IntegerArgumentType::new, new Integer[] {}, int.class, Integer.class); + registerNumericWithTuple(FloatArgumentType::new, new Float[] {}, float.class, Float.class); + registerNumericWithTuple(DoubleArgumentType::new, new Double[] {}, double.class, Double.class); + registerNumericWithTuple(LongArgumentType::new, new Long[] {}, long.class, Long.class); + registerNumericWithTuple(ShortArgumentType::new, new Short[] {}, short.class, Short.class); + registerNumericWithTuple(ByteArgumentType::new, new Byte[] {}, byte.class, Byte.class); } } diff --git a/src/main/java/lanat/argumentTypes/MultipleNumbersArgumentType.java b/src/main/java/lanat/argumentTypes/MultipleNumbersArgumentType.java new file mode 100644 index 00000000..cafe72de --- /dev/null +++ b/src/main/java/lanat/argumentTypes/MultipleNumbersArgumentType.java @@ -0,0 +1,28 @@ +package lanat.argumentTypes; + +import lanat.ArgumentType; +import lanat.ArgumentTypeInfer; +import lanat.utils.Range; +import org.jetbrains.annotations.NotNull; + +/** + * An argument type that takes multiple numbers. + * @param The type of number that this argument type is. + */ +public class MultipleNumbersArgumentType extends TupleArgumentType { + /** + * Creates a new {@link TupleArgumentType} with the specified range of values that the argument will take. + * @param range The range of values that the argument will take. + * @param defaultValue The default value of the argument. This will be used if no values are provided. + * @throws lanat.exceptions.ArgumentTypeInferException If the type of the default value is not supported. + */ + @SuppressWarnings("unchecked") + public MultipleNumbersArgumentType(@NotNull Range range, @NotNull Ti[] defaultValue) { + super( + range, + // we can infer the type of the argument type from the default value + (ArgumentType)ArgumentTypeInfer.get(defaultValue.getClass().getComponentType()), + defaultValue + ); + } +} diff --git a/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java b/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java index 291c46b4..c19af4c5 100644 --- a/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java +++ b/src/main/java/lanat/argumentTypes/MultipleStringsArgumentType.java @@ -1,26 +1,23 @@ package lanat.argumentTypes; -import lanat.ArgumentType; import lanat.utils.Range; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * An argument type that takes multiple strings. */ -public class MultipleStringsArgumentType extends ArgumentType { - @Override - public @NotNull Range getRequiredArgValueCount() { - return Range.AT_LEAST_ONE; +public class MultipleStringsArgumentType extends TupleArgumentType { + /** + * Creates a new {@link TupleArgumentType} with the specified range of values that the argument will take. + * @param range The range of values that the argument will take. + */ + public MultipleStringsArgumentType(@NotNull Range range) { + super(range, new StringArgumentType(), new String[0]); } + // no need for anything fancy here, simply return the args @Override public @NotNull String[] parseValues(@NotNull String @NotNull [] args) { return args; } - - @Override - public @Nullable String getDescription() { - return "Accepts multiple strings."; - } } diff --git a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java index 907395a9..1c876e19 100644 --- a/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java +++ b/src/main/java/lanat/argumentTypes/NumberRangeArgumentType.java @@ -21,6 +21,7 @@ public class NumberRangeArgumentType> extends A * Creates a new number range argument type. * @param min The minimum value. * @param max The maximum value. + * @throws lanat.exceptions.ArgumentTypeInferException If the type of the default value is not supported. */ @SuppressWarnings("unchecked") public NumberRangeArgumentType(@NotNull T min, @NotNull T max) { @@ -28,13 +29,7 @@ public NumberRangeArgumentType(@NotNull T min, @NotNull T max) { throw new IllegalArgumentException("min must be less than or equal to max"); } - final var typeInferred = ArgumentTypeInfer.get(min.getClass()); - - if (typeInferred == null) { - throw new IllegalArgumentException("Unsupported type: " + min.getClass().getName()); - } - - this.argumentType = (ArgumentType)typeInferred; + this.argumentType = (ArgumentType)ArgumentTypeInfer.get(min.getClass()); this.registerSubType(this.argumentType); this.min = min; diff --git a/src/main/java/lanat/argumentTypes/TupleArgumentType.java b/src/main/java/lanat/argumentTypes/TupleArgumentType.java index 4b95155a..73d52b51 100644 --- a/src/main/java/lanat/argumentTypes/TupleArgumentType.java +++ b/src/main/java/lanat/argumentTypes/TupleArgumentType.java @@ -12,12 +12,32 @@ * Shows a properly formatted description and representation. * @param the type of the value that the argument will take */ -public abstract class TupleArgumentType extends ArgumentType { +public abstract class TupleArgumentType extends ArgumentType { private final @NotNull Range argCount; + private final @NotNull ArgumentType argumentType; - public TupleArgumentType(@NotNull Range range, @NotNull T initialValue) { - super(initialValue); + /** + * Creates a new {@link TupleArgumentType} with the specified range and argument type. + * @param range The range of values that the argument will take. + * @param argumentType The argument type that will be used to parse the values. + * @param defaultValue The default value of the argument. This will be used if no values are provided. + */ + public TupleArgumentType(@NotNull Range range, @NotNull ArgumentType argumentType, @NotNull T[] defaultValue) { + super(defaultValue); this.argCount = range; + this.registerSubType(this.argumentType = argumentType); + } + + @SuppressWarnings("unchecked") + @Override + public T @Nullable [] parseValues(@NotNull String... args) { + var result = new Object[args.length]; + + for (int i = 0; i < args.length; i++) { + result[i] = this.argumentType.parseValues(args[i]); + } + + return (T[])result; } @Override @@ -26,14 +46,18 @@ public TupleArgumentType(@NotNull Range range, @NotNull T initialValue) { } @Override - public @NotNull TextFormatter getRepresentation() { - return new TextFormatter(this.getValue().getClass().getSimpleName()) + public @Nullable TextFormatter getRepresentation() { + var argTypeRepr = this.argumentType.getRepresentation(); + if (argTypeRepr == null) + return null; + + return argTypeRepr .concat(new TextFormatter(this.argCount.getRegexRange()).withForegroundColor(Color.BRIGHT_YELLOW)); } @Override public @Nullable String getDescription() { return "Takes " + this.argCount.getMessage("value") - + " of type " + this.getInitialValue().getClass().getSimpleName() + "."; + + " of type " + this.argumentType.getRepresentation() + "."; } } \ No newline at end of file diff --git a/src/main/java/lanat/exceptions/ArgumentTypeInferException.java b/src/main/java/lanat/exceptions/ArgumentTypeInferException.java new file mode 100644 index 00000000..9e67177f --- /dev/null +++ b/src/main/java/lanat/exceptions/ArgumentTypeInferException.java @@ -0,0 +1,7 @@ +package lanat.exceptions; + +public class ArgumentTypeInferException extends ArgumentTypeException { + public ArgumentTypeInferException(Class clazz) { + super("No argument type found for type: " + clazz.getName()); + } +} diff --git a/src/main/java/lanat/utils/UtlReflection.java b/src/main/java/lanat/utils/UtlReflection.java index a1e8714e..687bcc27 100644 --- a/src/main/java/lanat/utils/UtlReflection.java +++ b/src/main/java/lanat/utils/UtlReflection.java @@ -64,5 +64,4 @@ public static Stream getMethods(Class clazz) { return UtlReflection.getMethods(clazz.getSuperclass()); return Stream.of(clazz.getDeclaredMethods()); } - } diff --git a/src/test/java/lanat/test/UnitTests.java b/src/test/java/lanat/test/UnitTests.java index 4f4c308c..65f3c366 100644 --- a/src/test/java/lanat/test/UnitTests.java +++ b/src/test/java/lanat/test/UnitTests.java @@ -6,7 +6,6 @@ import lanat.argumentTypes.CounterArgumentType; import lanat.argumentTypes.IntegerArgumentType; import lanat.argumentTypes.StringArgumentType; -import lanat.argumentTypes.TupleArgumentType; import lanat.helpRepresentation.HelpFormatter; import lanat.utils.Range; import lanat.utils.displayFormatter.TextFormatter; @@ -17,9 +16,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -class StringJoiner extends TupleArgumentType { - public StringJoiner() { - super(Range.from(1).to(3), ""); +class StringJoiner extends ArgumentType { + @Override + public @NotNull Range getRequiredArgValueCount() { + return Range.from(1).to(3); } @Override diff --git a/src/test/java/lanat/test/units/TestArgumentTypes.java b/src/test/java/lanat/test/units/TestArgumentTypes.java index 7b4a1873..c04e4373 100644 --- a/src/test/java/lanat/test/units/TestArgumentTypes.java +++ b/src/test/java/lanat/test/units/TestArgumentTypes.java @@ -4,6 +4,7 @@ import lanat.argumentTypes.*; import lanat.test.TestingParser; import lanat.test.UnitTests; +import lanat.utils.Range; import org.junit.jupiter.api.Test; import java.io.File; @@ -24,7 +25,10 @@ protected TestingParser setParser() { 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 MultipleStringsArgumentType(Range.AT_LEAST_ONE), "multiple-strings")); + this.addArgument(Argument.create(new MultipleNumbersArgumentType<>( + Range.AT_LEAST_ONE, new Integer[] { 10101 }), "multiple-ints") + ); this.addArgument(Argument.create(new FileArgumentType(true), "file")); this.addArgument(Argument.create(new EnumArgumentType<>(TestEnum.TWO), "enum")); this.addArgument(Argument.create(new KeyValuesArgumentType<>(new IntegerArgumentType()), "key-value")); @@ -75,6 +79,13 @@ public void testStrings() { assertArrayEquals(new String[] { "hello world" }, this.parseArg("multiple-strings", "'hello world'")); } + @Test + public void testNumbers() { + assertArrayEquals(new Integer[] { 4 }, this.parseArg("multiple-ints", "4")); + assertArrayEquals(new Integer[] { 4, 5, 6 }, this.parseArg("multiple-ints", "4 5 6")); + assertArrayEquals(new Integer[] { 10101 }, this.parseArg("multiple-ints", "")); + } + @Test public void testFile() { assertNull(this.parseArg("file", "hello.txt")); From 90bf31226374b33bad80dd3abc527996e8bed0c4 Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Wed, 18 Oct 2023 18:17:13 +0200 Subject: [PATCH 08/15] fix: Command Template generation throwing exception when attempting to cast array types from parsed values. add: test for array parsed values --- src/main/java/lanat/ArgumentParser.java | 46 +++++++++++++++++-- .../manualTests/CommandTemplateExample.java | 2 + .../lanat/test/manualTests/ManualTests.java | 15 ++---- .../units/commandTemplates/CmdTemplates.java | 3 ++ .../units/commandTemplates/TestFromInto.java | 18 ++++++-- 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java index 21d4073f..aa45bb96 100644 --- a/src/main/java/lanat/ArgumentParser.java +++ b/src/main/java/lanat/ArgumentParser.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -131,7 +132,11 @@ public static ArgumentParser from(@NotNull Class temp */ public static @NotNull T parseFromInto(@NotNull Class templateClass, @NotNull CLInput input) { - return ArgumentParser.parseFromInto(templateClass, input, opts -> opts.printErrors().exitIfErrors()); + return ArgumentParser.parseFromInto( + templateClass, + input, + opts -> opts.printErrors().exitIfErrors().printHelpIfNoInput() + ); } /** @@ -366,7 +371,7 @@ private static T into( // 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); + 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) @@ -379,7 +384,7 @@ private static T into( instance, f.getType().isAssignableFrom(Optional.class) ? parsedValue - : parsedValue.orElse(null) + : AfterParseOptions.into$getNewFieldValue(f, parsedValue) ); } catch (IllegalArgumentException e) { if (parsedValue.isEmpty()) @@ -390,7 +395,7 @@ private static T into( throw new IncompatibleCommandTemplateType( "Field '" + f.getName() + "' of type '" + f.getType().getSimpleName() + "' is not " - + "compatible with the type (" + parsedValue.getClass().getSimpleName() + ") of the " + + "compatible with the type (" + parsedValue.get().getClass().getSimpleName() + ") of the " + "parsed argument '" + argName + "'" ); @@ -456,5 +461,38 @@ private static T into( throw new RuntimeException(e); } } + + /** + * {@link #into(Class)} helper method. Returns the new value for the given field based on the parsed value. + * If the parsed value is {@code null}, this method will return {@code null} as well. + * If both the field and the parsed value are arrays, this method will return a new array with the same type. + * @param commandAccesorField The field to get the new value for. + * @param parsedValue The parsed value to get the new value from. + * @return The new value for the given field based on the parsed value. This will be {@code null} if the parsed + * value is {@code null}. + */ + private static Object into$getNewFieldValue( + @NotNull Field commandAccesorField, + @NotNull Optional parsedValue + ) { + if (parsedValue.isEmpty()) + return null; + + final Object value = parsedValue.get(); + + if (!(commandAccesorField.getType().isArray() && value.getClass().isArray())) + return value; + + + // handle array types + final var fieldType = commandAccesorField.getType().getComponentType(); + final var originalArray = (Object[])value; // to get rid of warnings + + // create a new array of the same type as the field. + var newArray = (Object[])Array.newInstance(fieldType, Array.getLength(originalArray)); + // copy the values from the original array to the new array + System.arraycopy(originalArray, 0, newArray, 0, originalArray.length); + return newArray; + } } } \ 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 index e3114f89..3a5060a7 100644 --- a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java +++ b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java @@ -32,6 +32,8 @@ public CommandTemplateExample() {} @Argument.Define(names = "arg1a", argType = StringArgumentType.class) public String arg1copy; + @Argument.Define + public Byte[] bytes; @CommandAccessor public MySubCommand subCommand; diff --git a/src/test/java/lanat/test/manualTests/ManualTests.java b/src/test/java/lanat/test/manualTests/ManualTests.java index bf66bdc9..806a84f8 100644 --- a/src/test/java/lanat/test/manualTests/ManualTests.java +++ b/src/test/java/lanat/test/manualTests/ManualTests.java @@ -2,28 +2,20 @@ import lanat.ArgumentParser; import lanat.CLInput; -import lanat.Command; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; +import java.util.Arrays; public final class ManualTests { @Test public void main() { - String input = ""; + String input = "--bytes 5 12 89"; // 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() - .printHelpIfNoInput() - .into(CommandTemplateExample.class); + var parsed = ArgumentParser.parseFromInto(CommandTemplateExample.class, CLInput.from(input)); parsed.string .ifPresentOrElse( @@ -35,5 +27,6 @@ public void main() { System.out.println(parsed.subCommand.counter); System.out.println(parsed.subCommand.anotherSubCommand.counter); System.out.println(parsed.stdin); + System.out.println(Arrays.toString(parsed.bytes)); } } \ No newline at end of file diff --git a/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java b/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java index 8d8a9b50..aa4d2578 100644 --- a/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java +++ b/src/test/java/lanat/test/units/commandTemplates/CmdTemplates.java @@ -82,5 +82,8 @@ public static class CmdTemplate4 extends CommandTemplate { @Argument.Define public Double number2; + + @Argument.Define + public Byte[] bytes; } } diff --git a/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java b/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java index e3dd43de..6458b9f6 100644 --- a/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java +++ b/src/test/java/lanat/test/units/commandTemplates/TestFromInto.java @@ -3,10 +3,7 @@ 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.argumentTypes.*; import lanat.exceptions.CommandTemplateException; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; @@ -57,7 +54,6 @@ public void testNestedCommands() { assertEquals(52, result.cmd2.cmd3.number); } - @Test @DisplayName("test type inference for fields argument types") public void testTypeInference() { @@ -67,5 +63,17 @@ public void testTypeInference() { assertTrue(result.getArgument("text").argType instanceof StringArgumentType); assertTrue(result.getArgument("flag").argType instanceof BooleanArgumentType); assertTrue(result.getArgument("number2").argType instanceof DoubleArgumentType); + assertTrue(result.getArgument("bytes").argType instanceof MultipleNumbersArgumentType); + } + + @Test + @DisplayName("test array parsed values are properly converted") + public void testArrayParsedValues() { + final var result = ArgumentParser.parseFromInto( + CmdTemplates.CmdTemplate4.class, + CLInput.from("--bytes 5 12 89") + ); + + assertArrayEquals(new Byte[] {5, 12, 89}, result.bytes); } } \ No newline at end of file From 9428af5cac6321398bb81f8a45a287369ab92b2e Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Wed, 18 Oct 2023 19:01:10 +0200 Subject: [PATCH 09/15] change default option chain for ArgumentParser#parseFromInto --- src/main/java/lanat/ArgumentParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java index aa45bb96..349248c4 100644 --- a/src/main/java/lanat/ArgumentParser.java +++ b/src/main/java/lanat/ArgumentParser.java @@ -135,7 +135,7 @@ public static ArgumentParser from(@NotNull Class temp return ArgumentParser.parseFromInto( templateClass, input, - opts -> opts.printErrors().exitIfErrors().printHelpIfNoInput() + opts -> opts.printErrors().exitIfErrors().printHelpIfNoInput().exitIfNoInput() ); } From 02923c1b46f53496e64e5901557b9e12a766a4a6 Mon Sep 17 00:00:00 2001 From: DarviL Date: Thu, 19 Oct 2023 14:02:35 +0200 Subject: [PATCH 10/15] feat: Made errors clearer when there's a type mismatch on array types for cmd templates. Also, when attempting to instantiate a class fails. fix: primitive types being allowed to use for cmd template types, causing errors. refactor: remove most useless UtlString#surround usages. docs: add javadoc to CommandBuildHelper#arg --- src/main/java/lanat/ArgumentBuilder.java | 2 +- src/main/java/lanat/ArgumentParser.java | 19 ++++++++---- src/main/java/lanat/ArgumentTypeInfer.java | 29 ++++++++++++------- src/main/java/lanat/Command.java | 2 +- src/main/java/lanat/CommandTemplate.java | 8 +++++ .../ObjectAlreadyExistsException.java | 8 ++--- .../exceptions/ObjectNotFoundException.java | 12 ++------ .../helpRepresentation/descriptions/Tag.java | 3 +- .../exceptions/InvalidRouteException.java | 5 ++-- .../exceptions/MalformedTagException.java | 5 +--- .../NoDescriptionDefinedException.java | 3 +- .../exceptions/UnknownTagException.java | 3 +- .../descriptions/tags/ColorTag.java | 5 ++-- .../descriptions/tags/FormatTag.java | 2 +- src/main/java/lanat/utils/UtlReflection.java | 20 ++++++++++++- 15 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/main/java/lanat/ArgumentBuilder.java b/src/main/java/lanat/ArgumentBuilder.java index 92c8142b..5c20e4ea 100644 --- a/src/main/java/lanat/ArgumentBuilder.java +++ b/src/main/java/lanat/ArgumentBuilder.java @@ -58,7 +58,7 @@ public class ArgumentBuilder, TInner> { /** * Builds an {@link Argument} from the specified field annotated with {@link Argument.Define}. - * Note that this doesn't set the argument type. + * Note that this doesn't set the argument type. Use {@link #setArgTypeFromField(Field)} for that. * * @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 diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java index 349248c4..02368cd6 100644 --- a/src/main/java/lanat/ArgumentParser.java +++ b/src/main/java/lanat/ArgumentParser.java @@ -488,11 +488,20 @@ private static T into( final var fieldType = commandAccesorField.getType().getComponentType(); final var originalArray = (Object[])value; // to get rid of warnings - // create a new array of the same type as the field. - var newArray = (Object[])Array.newInstance(fieldType, Array.getLength(originalArray)); - // copy the values from the original array to the new array - System.arraycopy(originalArray, 0, newArray, 0, originalArray.length); - return newArray; + try { + // create a new array of the same type as the field. + var newArray = (Object[])Array.newInstance(fieldType, Array.getLength(originalArray)); + + // copy the values from the original array to the new array + System.arraycopy(originalArray, 0, newArray, 0, originalArray.length); + + return newArray; + } catch (ClassCastException e) { + throw new IncompatibleCommandTemplateType( + "Field '" + commandAccesorField.getName() + "' of type '" + commandAccesorField.getType().getSimpleName() + + "' is not compatible with the type (" + fieldType.arrayType() + ") of the parsed argument" + ); + } } } } \ No newline at end of file diff --git a/src/main/java/lanat/ArgumentTypeInfer.java b/src/main/java/lanat/ArgumentTypeInfer.java index 5c1efd66..7942c97f 100644 --- a/src/main/java/lanat/ArgumentTypeInfer.java +++ b/src/main/java/lanat/ArgumentTypeInfer.java @@ -6,7 +6,6 @@ import org.jetbrains.annotations.NotNull; import java.io.File; -import java.util.Arrays; import java.util.HashMap; import java.util.function.Supplier; @@ -28,6 +27,9 @@ public static void register(@NotNull Supplier> type, @ throw new IllegalArgumentException("Must specify at least one type to infer the argument type for."); for (Class clazz : infer) { + if (clazz.isArray() && clazz.getComponentType().isPrimitive()) + throw new IllegalArgumentException("Cannot infer argument type for primitive array type: " + clazz.getName()); + if (ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.containsKey(clazz)) throw new IllegalArgumentException("Argument type already registered for type: " + clazz.getName()); @@ -52,21 +54,28 @@ public static ArgumentType get(@NotNull Class clazz) { /** * Registers a numeric argument type with the specified tuple type as well. + * Note that for arrays, only the non-primitive types are inferred. * @param type The type of the numeric argument type. * @param array The default value of the numeric argument type. - * @param infer The non-array types to infer the argument type for. + * @param inferPrimitive The non-array types to infer the argument type for. * @param The type of the numeric type. * @param The type of the tuple argument type. */ private static > - void registerNumericWithTuple(@NotNull Supplier type, Ti[] array, @NotNull Class... infer) { - ArgumentTypeInfer.register(type, infer); - ArgumentTypeInfer.register( - () -> new MultipleNumbersArgumentType<>(DEFAULT_TYPE_RANGE, array), - Arrays.stream(infer) - .map(Class::arrayType) - .toArray(Class[]::new) - ); + void registerNumericWithTuple( + @NotNull Supplier type, + @NotNull Ti[] array, + @NotNull Class inferPrimitive, + @NotNull Class infer + ) { + assert !infer.isPrimitive() && inferPrimitive.isPrimitive() + : "Infer must be a primitive type and inferPrimitive must be a non-primitive type."; + + // register both the primitive and non-primitive types + ArgumentTypeInfer.register(type, inferPrimitive, infer); + + // register the array type (only the non-primitive type) + ArgumentTypeInfer.register(() -> new MultipleNumbersArgumentType<>(DEFAULT_TYPE_RANGE, array), infer.arrayType()); } // add some default argument types. diff --git a/src/main/java/lanat/Command.java b/src/main/java/lanat/Command.java index 6c752dcf..41fe60a0 100644 --- a/src/main/java/lanat/Command.java +++ b/src/main/java/lanat/Command.java @@ -199,7 +199,7 @@ public void addNames(@NotNull String... names) { .map(UtlString::requireValidName) .peek(newName -> { if (this.hasName(newName)) - throw new IllegalArgumentException("Name " + UtlString.surround(newName) + " is already used by this command."); + throw new IllegalArgumentException("Name '" + newName + "' is already used by this command."); }) .forEach(this.names::add); diff --git a/src/main/java/lanat/CommandTemplate.java b/src/main/java/lanat/CommandTemplate.java index 2a79e62e..bb4df964 100644 --- a/src/main/java/lanat/CommandTemplate.java +++ b/src/main/java/lanat/CommandTemplate.java @@ -124,6 +124,14 @@ public abstract class CommandTemplate { * to get the argument builder corresponding to an argument with a given name. */ public record CommandBuildHelper(@NotNull Command cmd, @NotNull List> args) { + /** + * Returns the argument builder corresponding to the argument with the given name. + * This is a helper method to get the argument builder from the list of argument builders ({@link #args}). + * @param name The name of the argument. + * @return The argument builder corresponding to the argument with the given name. + * @param The type of the argument. + * @param The type of the value passed to the argument. + */ @SuppressWarnings("unchecked") public , TInner> ArgumentBuilder arg(@NotNull String name) { diff --git a/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java b/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java index b115b2a8..8eda309c 100644 --- a/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java +++ b/src/main/java/lanat/exceptions/ObjectAlreadyExistsException.java @@ -2,7 +2,6 @@ import lanat.NamedWithDescription; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** @@ -17,12 +16,9 @@ public ObjectAlreadyExistsException( { super( typeName - + " " - + UtlString.surround(obj.getName()) - + " already exists in " + + " '" + obj.getName() + "' already exists in " + UtlReflection.getSimpleName(container.getClass()) - + " " - + UtlString.surround(container.getName()) + + " '" + container.getName() + "'" ); } } diff --git a/src/main/java/lanat/exceptions/ObjectNotFoundException.java b/src/main/java/lanat/exceptions/ObjectNotFoundException.java index 8b888995..b0bc84a1 100644 --- a/src/main/java/lanat/exceptions/ObjectNotFoundException.java +++ b/src/main/java/lanat/exceptions/ObjectNotFoundException.java @@ -2,7 +2,6 @@ import lanat.NamedWithDescription; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** @@ -17,12 +16,9 @@ public ObjectNotFoundException( { super( typeName - + " " - + UtlString.surround(obj.getName()) - + " was not found in " + + " '" + obj.getName() + "' was not found in " + UtlReflection.getSimpleName(container.getClass()) - + " " - + UtlString.surround(container.getName()) + + " '" + container.getName() + "'" ); } @@ -31,8 +27,6 @@ public ObjectNotFoundException(@NotNull String typeName, @NotNull NamedWithDescr } public ObjectNotFoundException(@NotNull String typeName, @NotNull String name) { - super( - typeName + " " + UtlString.surround(name) + " was not found" - ); + super(typeName + " '" + name + "' was not found"); } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/Tag.java b/src/main/java/lanat/helpRepresentation/descriptions/Tag.java index 212e6c03..80e94596 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/Tag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/Tag.java @@ -7,7 +7,6 @@ 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; @@ -53,7 +52,7 @@ public static String getTagNameFromTagClass(Class tagClass) { .findFirst() .map(Map.Entry::getKey) .orElseThrow(() -> - new IllegalStateException("Tag class " + UtlString.surround(tagClass.getName()) + " is not registered") + new IllegalStateException("Tag class '" + tagClass.getName() + "' is not registered") ); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java index 42adf705..f594f2db 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/InvalidRouteException.java @@ -3,7 +3,6 @@ import lanat.NamedWithDescription; import lanat.exceptions.LanatException; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -15,8 +14,8 @@ public InvalidRouteException(@NotNull NamedWithDescription user, @Nullable Strin public InvalidRouteException(@NotNull NamedWithDescription user, @Nullable String value, @Nullable String message) { super( - "invalid route value " + UtlString.surround(value) - + " for " + UtlReflection.getSimpleName(user.getClass()) + " " + UtlString.surround(user.getName()) + "invalid route value '" + value + "' for " + + UtlReflection.getSimpleName(user.getClass()) + " '" + user.getName() + "'" + (message == null ? "" : ": " + message) ); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java index cd8064c3..bd8105e5 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/MalformedTagException.java @@ -2,7 +2,6 @@ import lanat.exceptions.LanatException; import lanat.helpRepresentation.descriptions.Tag; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,9 +9,7 @@ public class MalformedTagException extends LanatException { public MalformedTagException(@NotNull Class tagClass, @Nullable String reason) { super( - "Tag " - + UtlString.surround(Tag.getTagNameFromTagClass(tagClass)) - + " is malformed" + "Tag '" + Tag.getTagNameFromTagClass(tagClass) + "' is malformed" + (reason == null ? "" : ": " + reason) ); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java index afdb1d00..a0188ba2 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/NoDescriptionDefinedException.java @@ -3,7 +3,6 @@ import lanat.NamedWithDescription; import lanat.exceptions.LanatException; import lanat.utils.UtlReflection; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** Thrown when a description was not defined for an object. */ @@ -11,7 +10,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()) + " '" + user.getName() + "'" ); } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java index ee6173fb..0773ccc4 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/exceptions/UnknownTagException.java @@ -1,12 +1,11 @@ package lanat.helpRepresentation.descriptions.exceptions; import lanat.exceptions.LanatException; -import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; /** Thrown when a tag with an unknown name is attempted to be used. */ public class UnknownTagException extends LanatException { public UnknownTagException(@NotNull String tagName) { - super("tag " + UtlString.surround(tagName) + " does not exist"); + super("tag '" + tagName + "' does not exist"); } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java index df96a573..5089aa62 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/ColorTag.java @@ -55,8 +55,7 @@ public class ColorTag extends Tag { final String[] split = UtlString.split(value, ':'); if (split.length != 2) throw new MalformedTagException( - ColorTag.class, "invalid color format " + UtlString.surround(value) - + " (expected format: 'foreground:background')" + ColorTag.class, "invalid color format '" + value + "' (expected format: 'foreground:background')" ); return ColorTag.getColor(split[0]).fg() + ColorTag.getColor(split[1]).bg(); @@ -80,7 +79,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(ColorTag.class, "unknown color name " + UtlString.surround(colorName)); + default -> throw new MalformedTagException(ColorTag.class, "unknown color name '" + colorName + "'"); }; } } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java b/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java index 0674a667..01068569 100644 --- a/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java +++ b/src/main/java/lanat/helpRepresentation/descriptions/tags/FormatTag.java @@ -62,7 +62,7 @@ private static FormatOption getFormat(@NotNull String formatName) { case "hidden", "h" -> FormatOption.HIDDEN; case "strike", "s" -> FormatOption.STRIKE_THROUGH; default -> - throw new MalformedTagException(FormatTag.class, "unknown format name " + UtlString.surround(formatName)); + throw new MalformedTagException(FormatTag.class, "unknown format name '" + formatName + "'"); }; } } diff --git a/src/main/java/lanat/utils/UtlReflection.java b/src/main/java/lanat/utils/UtlReflection.java index 687bcc27..bc6800df 100644 --- a/src/main/java/lanat/utils/UtlReflection.java +++ b/src/main/java/lanat/utils/UtlReflection.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.stream.Stream; @@ -48,7 +49,24 @@ public static boolean hasParameters(Method method, Class... parameters) { public static T instantiate(Class clazz, Object... args) { try { return clazz.getDeclaredConstructor().newInstance(args); - } catch (ReflectiveOperationException e) { + } catch (NoSuchMethodException e) { + throw new RuntimeException("Unable to find a public constructor for the class '" + clazz.getName() + + """ + '. Please, make sure: + - This class has a public constructor with no arguments. (Or no constructor at all) + - This is a static class. (Not an inner class)""" + ); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Unable to gain access to the class '" + clazz.getName() + + "'. Please, make sure this class is visible to Lanat." + ); + } catch (InstantiationException e) { + throw new RuntimeException( + "Unable to instantiate the class '" + clazz.getName() + + "'. Please, make sure this class is not abstract." + ); + } catch (InvocationTargetException e) { throw new RuntimeException(e); } } From f1151b68001506b7b64522b31f0c6d5ffb905381 Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Thu, 19 Oct 2023 17:53:15 +0200 Subject: [PATCH 11/15] fix: default version argument showing "Version: null" if no version was defined. Now shows "unknown". fix: synopsis appending an extra space character when printing commands. refactor: small tweaks to nullability checks. docs: add some more javadocs --- src/main/java/lanat/Argument.java | 2 +- src/main/java/lanat/ArgumentAdder.java | 2 +- src/main/java/lanat/ArgumentGroupAdder.java | 2 +- src/main/java/lanat/ArgumentParser.java | 5 +++- src/main/java/lanat/Command.java | 30 ++++++++++++++++--- .../helpRepresentation/ArgumentRepr.java | 11 ++++--- .../lanat/helpRepresentation/CommandRepr.java | 10 +++---- .../helpRepresentation/LayoutGenerators.java | 2 +- .../descriptions/DescriptionFormatter.java | 10 +++---- src/main/java/lanat/utils/UtlMisc.java | 23 ++++++++++++++ .../manualTests/CommandTemplateExample.java | 5 ++-- .../lanat/test/manualTests/ManualTests.java | 2 +- 12 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/main/java/lanat/Argument.java b/src/main/java/lanat/Argument.java index a01876c0..a5cf726b 100644 --- a/src/main/java/lanat/Argument.java +++ b/src/main/java/lanat/Argument.java @@ -476,7 +476,7 @@ public void parseValues(short tokenIndex, @NotNull String... values) { */ public @Nullable TInner finishParsing() { final TInner finalValue = this.argType.getFinalValue(); - final TInner defaultValue = this.defaultValue == null ? this.argType.getInitialValue() : this.defaultValue; + final TInner defaultValue = UtlMisc.nonNullOrElse(this.defaultValue, this.argType.getInitialValue()); /* 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) */ diff --git a/src/main/java/lanat/ArgumentAdder.java b/src/main/java/lanat/ArgumentAdder.java index dcfc52bd..2f799e54 100644 --- a/src/main/java/lanat/ArgumentAdder.java +++ b/src/main/java/lanat/ArgumentAdder.java @@ -39,7 +39,7 @@ void addArgument(@NotNull ArgumentBuilder argument) { @NotNull List<@NotNull Argument> getArguments(); /** - * Checks that all the arguments in this container have unique names. + * Checks that all the arguments in this container are unique. * @throws ArgumentAlreadyExistsException if there are two arguments with the same name */ default void checkUniqueArguments() { diff --git a/src/main/java/lanat/ArgumentGroupAdder.java b/src/main/java/lanat/ArgumentGroupAdder.java index f9cda9d8..6f568dda 100644 --- a/src/main/java/lanat/ArgumentGroupAdder.java +++ b/src/main/java/lanat/ArgumentGroupAdder.java @@ -24,7 +24,7 @@ public interface ArgumentGroupAdder extends NamedWithDescription { @NotNull List<@NotNull ArgumentGroup> getGroups(); /** - * Checks that all the argument groups in this container have unique names. + * Checks that all the argument groups in this container are unique. * @throws ArgumentGroupAlreadyExistsException if there are two argument groups with the same name */ default void checkUniqueGroups() { diff --git a/src/main/java/lanat/ArgumentParser.java b/src/main/java/lanat/ArgumentParser.java index 349248c4..95346ee3 100644 --- a/src/main/java/lanat/ArgumentParser.java +++ b/src/main/java/lanat/ArgumentParser.java @@ -4,6 +4,7 @@ import lanat.exceptions.IncompatibleCommandTemplateType; import lanat.parsing.TokenType; import lanat.parsing.errors.ErrorHandler; +import lanat.utils.UtlMisc; import lanat.utils.UtlReflection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -244,7 +245,9 @@ public void setVersion(@NotNull String version) { */ public void addVersionArgument() { this.addArgument(Argument.createOfBoolType("version") - .onOk(t -> System.out.println("Version: " + this.getVersion())) + .onOk(t -> + System.out.println("Version: " + UtlMisc.nonNullOrElse(this.getVersion(), "unknown")) + ) .withDescription("Shows the version of this program.") .allowsUnique() ); diff --git a/src/main/java/lanat/Command.java b/src/main/java/lanat/Command.java index 6c752dcf..f96797b7 100644 --- a/src/main/java/lanat/Command.java +++ b/src/main/java/lanat/Command.java @@ -268,7 +268,7 @@ public void addError(@NotNull String message, @NotNull ErrorLevel level) { * 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() { + boolean uniqueArgumentReceivedValue() { return this.arguments.stream().anyMatch(a -> a.getUsageCount() >= 1 && a.isUniqueAllowed()) || this.subCommands.stream().anyMatch(Command::uniqueArgumentReceivedValue); } @@ -362,6 +362,12 @@ private void inheritProperties(@NotNull Command parent) { this.from$invokeAfterInitMethod(cmdTemplate); } + /** + * Invokes the {@link CommandTemplate#beforeInit(CommandTemplate.CommandBuildHelper)} method of the given command + * template class, if it exists. + * @param cmdTemplate The command template class to invoke the method from. + * @param argumentBuilders The argument builders that will be passed to the method. + */ private void from$invokeBeforeInitMethod( @NotNull Class cmdTemplate, @NotNull List> argumentBuilders @@ -382,6 +388,10 @@ private void inheritProperties(@NotNull Command parent) { }); } + /** + * Invokes the {@link CommandTemplate#afterInit(Command)} method of the given command template class, if it exists. + * @param cmdTemplate The command template class to invoke the method from. + */ private void from$invokeAfterInitMethod(@NotNull Class cmdTemplate) { Stream.of(cmdTemplate.getDeclaredMethods()) .filter(m -> UtlReflection.hasParameters(m, Command.class)) @@ -397,6 +407,10 @@ private void inheritProperties(@NotNull Command parent) { }); } + /** + * Passes certain properties to all the Sub-Commands of this command. + * @see #inheritProperties(Command) + */ void passPropertiesToChildren() { this.subCommands.forEach(c -> c.inheritProperties(this)); } @@ -418,6 +432,10 @@ public boolean equals(@NotNull Object obj) { return false; } + /** + * Checks that all the sub-commands in this container are unique. + * @throws CommandAlreadyExistsException if there are two commands with the same name + */ void checkUniqueSubCommands() { UtlMisc.requireUniqueElements(this.subCommands, c -> new CommandAlreadyExistsException(c, this)); } @@ -444,8 +462,6 @@ public void setOnOkCallback(@Nullable Consumer<@NotNull ParsedArguments> callbac @Override public void invokeCallbacks() { - this.subCommands.forEach(Command::invokeCallbacks); - if (this.shouldExecuteCorrectCallback()) { if (this.onCorrectCallback != null) this.onCorrectCallback.accept(this.getParsedArguments()); } else { @@ -457,8 +473,14 @@ public void invokeCallbacks() { .stream() .sorted((x, y) -> Argument.compareByPriority(x.getKey(), y.getKey())) // sort by priority when invoking callbacks! .forEach(e -> e.getKey().invokeCallbacks(e.getValue())); + + // invoke the callbacks of the Sub-Commands recursively + this.subCommands.forEach(Command::invokeCallbacks); } + /** + * Returns {@code true} if the {@link #onCorrectCallback} should be executed. + */ boolean shouldExecuteCorrectCallback() { return switch (this.getCallbackInvocationOption()) { case NO_ERROR_IN_COMMAND -> !this.hasExitErrorsNotIncludingSubCommands(); @@ -545,7 +567,7 @@ void tokenize(@NotNull String input) { } void parseTokens() { - // first we need to set the tokens of all tokenized subCommands + // first, we need to set the tokens of all tokenized subCommands Command cmd = this; do { cmd.parser.setTokens(cmd.tokenizer.getFinalTokens()); diff --git a/src/main/java/lanat/helpRepresentation/ArgumentRepr.java b/src/main/java/lanat/helpRepresentation/ArgumentRepr.java index a8bd1325..f348d4af 100644 --- a/src/main/java/lanat/helpRepresentation/ArgumentRepr.java +++ b/src/main/java/lanat/helpRepresentation/ArgumentRepr.java @@ -2,6 +2,7 @@ import lanat.Argument; import lanat.helpRepresentation.descriptions.DescriptionFormatter; +import lanat.utils.UtlMisc; import lanat.utils.displayFormatter.FormatOption; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; @@ -64,12 +65,10 @@ private ArgumentRepr() {} * @return the representation and description of the argument */ public static @Nullable String getDescription(@NotNull Argument arg) { - final String desc = DescriptionFormatter.parse(arg); - - if (desc == null) - return null; - - return ArgumentRepr.getRepresentation(arg) + ":\n" + HelpFormatter.indent(desc, arg); + return UtlMisc.nullOrElse( + DescriptionFormatter.parse(arg), + desc -> ArgumentRepr.getRepresentation(arg) + ":\n" + HelpFormatter.indent(desc, arg) + ); } /** diff --git a/src/main/java/lanat/helpRepresentation/CommandRepr.java b/src/main/java/lanat/helpRepresentation/CommandRepr.java index 5d2d31b4..4cdcb7da 100644 --- a/src/main/java/lanat/helpRepresentation/CommandRepr.java +++ b/src/main/java/lanat/helpRepresentation/CommandRepr.java @@ -2,6 +2,7 @@ import lanat.Command; import lanat.helpRepresentation.descriptions.DescriptionFormatter; +import lanat.utils.UtlMisc; import lanat.utils.displayFormatter.FormatOption; import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; @@ -52,11 +53,10 @@ private CommandRepr() {} * @return the parsed description of the command */ public static @Nullable String getDescription(@NotNull Command cmd) { - final String desc = DescriptionFormatter.parse(cmd); - - return desc == null - ? null - : CommandRepr.getRepresentation(cmd) + ":\n" + HelpFormatter.indent(desc, cmd); + return UtlMisc.nullOrElse( + DescriptionFormatter.parse(cmd), + desc -> CommandRepr.getRepresentation(cmd) + ":\n" + HelpFormatter.indent(desc, cmd) + ); } /** diff --git a/src/main/java/lanat/helpRepresentation/LayoutGenerators.java b/src/main/java/lanat/helpRepresentation/LayoutGenerators.java index bd3678df..f1951a64 100644 --- a/src/main/java/lanat/helpRepresentation/LayoutGenerators.java +++ b/src/main/java/lanat/helpRepresentation/LayoutGenerators.java @@ -77,7 +77,7 @@ private LayoutGenerators() {} } if (!cmd.getCommands().isEmpty()) - buffer.append(' ').append(CommandRepr.getSubCommandsRepresentation(cmd)); + buffer.append(CommandRepr.getSubCommandsRepresentation(cmd)); return buffer.toString(); } diff --git a/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java b/src/main/java/lanat/helpRepresentation/descriptions/DescriptionFormatter.java index 5c784b2c..03221581 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.UtlMisc; import lanat.utils.UtlString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -70,11 +71,10 @@ private DescriptionFormatter() {} */ public static @Nullable String parse(@NotNull T element) { - final var desc = element.getDescription(); - if (desc == null) - return null; - - return DescriptionFormatter.parse(element, desc); + return UtlMisc.nullOrElse( + element.getDescription(), + desc -> DescriptionFormatter.parse(element, desc) + ); } /** diff --git a/src/main/java/lanat/utils/UtlMisc.java b/src/main/java/lanat/utils/UtlMisc.java index c7af99f7..68c68899 100644 --- a/src/main/java/lanat/utils/UtlMisc.java +++ b/src/main/java/lanat/utils/UtlMisc.java @@ -4,6 +4,7 @@ import lanat.MultipleNamesAndDescription; import lanat.exceptions.ObjectAlreadyExistsException; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.function.Function; @@ -44,4 +45,26 @@ public static void requireUniqueElements( boolean equalsByNamesAndParentCmd(@NotNull T a, @NotNull T b) { return a.getParentCommand() == b.getParentCommand() && a.getNames().stream().anyMatch(b::hasName); } + + /** + * Returns {@code obj} if it is not {@code null}, otherwise returns {@code defaultObj}. + * @param obj The object to check + * @param defaultObj The object to return if {@code obj} is {@code null} + * @return {@code obj} if it is not {@code null}, otherwise returns {@code defaultObj} + * @param The type of the objects + */ + public static @NotNull T nonNullOrElse(@Nullable T obj, @NotNull T defaultObj) { + return obj == null ? defaultObj : obj; + } + + /** + * Returns {@code null} if {@code obj} is {@code null}, otherwise returns the result of the given function. + * @param obj The object to check + * @param defaultObj The function to apply to {@code obj} if it is not {@code null} + * @return {@code null} if {@code obj} is {@code null}, otherwise returns the result of the given function + * @param The type of the objects + */ + public static @Nullable T nullOrElse(@Nullable T obj, @NotNull Function<@NotNull T, @NotNull T> defaultObj) { + return obj == null ? null : defaultObj.apply(obj); + } } diff --git a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java index 3a5060a7..f920fad8 100644 --- a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java +++ b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java @@ -19,7 +19,7 @@ public CommandTemplateExample() {} @Argument.Define(argType = StringArgumentType.class, description = "This is a string argument.") public Optional string; - @Argument.Define(description = "") + @Argument.Define(description = "", required = true) public double number = 12; @Argument.Define(argType = StdinArgumentType.class) @@ -47,6 +47,7 @@ public static void beforeInit(@NotNull CommandBuildHelper helper) { @InitDef public static void afterInit(@NotNull Command cmd) { cmd.addGroup(new ArgumentGroup("test-group") {{ + this.setExclusive(true); this.addArgument(cmd.getArgument("string")); this.addArgument(cmd.getArgument("number")); }}); @@ -54,7 +55,7 @@ public static void afterInit(@NotNull Command cmd) { @Command.Define(names = "sub-command", description = "This is a sub-command.") - public static class MySubCommand extends CommandTemplate { + public static class MySubCommand extends CommandTemplate.Default { public MySubCommand() {} @Argument.Define(argType = CounterArgumentType.class, description = "This is a counter", names = "c") diff --git a/src/test/java/lanat/test/manualTests/ManualTests.java b/src/test/java/lanat/test/manualTests/ManualTests.java index 806a84f8..3bcfb21f 100644 --- a/src/test/java/lanat/test/manualTests/ManualTests.java +++ b/src/test/java/lanat/test/manualTests/ManualTests.java @@ -10,7 +10,7 @@ public final class ManualTests { @Test public void main() { - String input = "--bytes 5 12 89"; + String input = "--help --version sub-command -h"; // write some stuff to stdin System.setIn(new ByteArrayInputStream("hello world\ngoodbye".getBytes())); From cf3fac496965a8fe3765aff535fc1f180ff0443d Mon Sep 17 00:00:00 2001 From: DarviL82 Date: Fri, 20 Oct 2023 17:51:20 +0200 Subject: [PATCH 12/15] small comments fixes --- src/main/java/lanat/helpRepresentation/HelpFormatter.java | 3 ++- src/main/java/lanat/helpRepresentation/LayoutItem.java | 5 +++-- src/main/java/lanat/utils/UtlString.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/lanat/helpRepresentation/HelpFormatter.java b/src/main/java/lanat/helpRepresentation/HelpFormatter.java index 72ea7dd7..77691117 100644 --- a/src/main/java/lanat/helpRepresentation/HelpFormatter.java +++ b/src/main/java/lanat/helpRepresentation/HelpFormatter.java @@ -20,7 +20,7 @@ * returns a string. *

*

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

* * @see LayoutItem @@ -32,6 +32,7 @@ public class HelpFormatter { public static boolean debugLayout = false; static { + // register the default tags before we start parsing descriptions Tag.initTags(); } diff --git a/src/main/java/lanat/helpRepresentation/LayoutItem.java b/src/main/java/lanat/helpRepresentation/LayoutItem.java index 97bf1df8..9abef01e 100644 --- a/src/main/java/lanat/helpRepresentation/LayoutItem.java +++ b/src/main/java/lanat/helpRepresentation/LayoutItem.java @@ -10,7 +10,7 @@ /** * 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 + * builder with some helper utilities for setting a {@link Function} that generates a {@link String} for a given * {@link Command}. * * @see HelpFormatter @@ -107,9 +107,10 @@ public LayoutItem margin(int margin) { *

* It is shown as: *

-	 * <title<:
+	 * <title>:
 	 *    <content>
 	 * 
+ * If no content is generated, the title is not shown. * * @param title the title of the layout item */ diff --git a/src/main/java/lanat/utils/UtlString.java b/src/main/java/lanat/utils/UtlString.java index ab91ece4..0965714c 100644 --- a/src/main/java/lanat/utils/UtlString.java +++ b/src/main/java/lanat/utils/UtlString.java @@ -72,7 +72,7 @@ private UtlString() {} final var indentBuff = new StringBuilder(); // buffer for the current indentation that will be added to the beginning of each line if needed int lineWidth = 0; // the current line width - boolean jumped = true; // true if a newline was added. starts off as true in case the string with indentation + boolean jumped = true; // true if a newline was added. starts off as true in case the string starts with indentation for (char chr : str.toCharArray()) { if (chr == ' ' || chr == '\t') { @@ -89,7 +89,7 @@ private UtlString() {} } endBuffer.append(wordBuff).append(chr); // make sure to not count escape sequences on the length! - lineWidth += UtlString.removeSequences(wordBuff.toString()).length() + 1; + lineWidth += UtlString.removeSequences(wordBuff.toString()).length() + 1; // +1 for the char we just added wordBuff.setLength(0); } From 82bf46d10462026cb29771ad2bcae8b3f975babd Mon Sep 17 00:00:00 2001 From: DarviL Date: Sun, 22 Oct 2023 21:03:02 +0200 Subject: [PATCH 13/15] fix: blank string not being detected as no input. feat: Now possible to specify in FileArgumentType if the file should be a directory or a regular file. feat: Improved the generated description and representation for FileArgumentType. --- src/main/java/lanat/CLInput.java | 2 +- .../argumentTypes/BooleanArgumentType.java | 1 + .../lanat/argumentTypes/ByteArgumentType.java | 1 + .../argumentTypes/DoubleArgumentType.java | 1 + .../lanat/argumentTypes/FileArgumentType.java | 80 ++++++++++++++++--- .../argumentTypes/FloatArgumentType.java | 1 + .../FromParseableArgumentType.java | 1 + .../argumentTypes/IntegerArgumentType.java | 1 + .../argumentTypes/KeyValuesArgumentType.java | 1 + .../lanat/argumentTypes/LongArgumentType.java | 1 + .../argumentTypes/NumberArgumentType.java | 1 + .../argumentTypes/ShortArgumentType.java | 1 + .../argumentTypes/StringArgumentType.java | 1 + src/main/java/lanat/utils/UtlMisc.java | 6 +- .../manualTests/CommandTemplateExample.java | 24 ++++-- .../lanat/test/manualTests/ManualTests.java | 2 +- 16 files changed, 104 insertions(+), 21 deletions(-) diff --git a/src/main/java/lanat/CLInput.java b/src/main/java/lanat/CLInput.java index f5e2ce63..a72e0cce 100644 --- a/src/main/java/lanat/CLInput.java +++ b/src/main/java/lanat/CLInput.java @@ -12,7 +12,7 @@ public final class CLInput { public final @NotNull String args; private CLInput(@NotNull String args) { - this.args = args; + this.args = args.trim(); } /** diff --git a/src/main/java/lanat/argumentTypes/BooleanArgumentType.java b/src/main/java/lanat/argumentTypes/BooleanArgumentType.java index ec676c09..b80800cb 100644 --- a/src/main/java/lanat/argumentTypes/BooleanArgumentType.java +++ b/src/main/java/lanat/argumentTypes/BooleanArgumentType.java @@ -8,6 +8,7 @@ /** * An argument type that is set in a true state if the argument was used. + * @see Boolean */ public class BooleanArgumentType extends ArgumentType { public BooleanArgumentType() { diff --git a/src/main/java/lanat/argumentTypes/ByteArgumentType.java b/src/main/java/lanat/argumentTypes/ByteArgumentType.java index c92c3ac4..ef93a9ef 100644 --- a/src/main/java/lanat/argumentTypes/ByteArgumentType.java +++ b/src/main/java/lanat/argumentTypes/ByteArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a byte value. + * @see Byte */ public class ByteArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/DoubleArgumentType.java b/src/main/java/lanat/argumentTypes/DoubleArgumentType.java index 89825e46..8b169465 100644 --- a/src/main/java/lanat/argumentTypes/DoubleArgumentType.java +++ b/src/main/java/lanat/argumentTypes/DoubleArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a double precision floating point number. + * @see Double */ public class DoubleArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/FileArgumentType.java b/src/main/java/lanat/argumentTypes/FileArgumentType.java index 28179b8a..f4941037 100644 --- a/src/main/java/lanat/argumentTypes/FileArgumentType.java +++ b/src/main/java/lanat/argumentTypes/FileArgumentType.java @@ -1,6 +1,7 @@ package lanat.argumentTypes; import lanat.ArgumentType; +import lanat.utils.displayFormatter.TextFormatter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -8,31 +9,88 @@ /** * An argument type that takes a file path and returns a {@link File} instance representing it. + * This argument type can also check if the file exists and if it is a regular file or a directory. + * @see File */ public class FileArgumentType extends ArgumentType { - private final boolean needsToExist; + public enum FileType { + REGULAR_FILE, + DIRECTORY, + ANY; + + private @NotNull String getName(boolean shortName) { + return switch (this) { + case REGULAR_FILE -> "file"; + case DIRECTORY -> "directory"; + case ANY -> shortName ? "(file/dir)" : "file or directory"; + }; + } + } + + private final boolean mustExist; + private final @NotNull FileType fileType; + /** * Creates a new file argument type. - * @param needsToExist whether the file needs to exist or not. If set to false, the file instance will be created - * even it doesn't exist. + * @param mustExist whether the file must exist or not + * @param fileType the type of the file (regular file, directory, or any) */ - public FileArgumentType(boolean needsToExist) { - this.needsToExist = needsToExist; + public FileArgumentType(boolean mustExist, @NotNull FileType fileType) { + this.mustExist = mustExist; + this.fileType = fileType; + } + + /** + * Creates a new file argument type which accepts any kind of file. + * @param mustExist whether the file must exist or not + */ + public FileArgumentType(boolean mustExist) { + this(mustExist, FileType.ANY); + } + + /** + * Checks if the file is valid. This method may add errors to the type. + * @param file the file to check + * @return whether the file is valid or not + */ + protected boolean checkFile(@NotNull File file) { + if (this.mustExist && !file.exists()) { + this.addError("File does not exist."); + return false; + } + + if (this.fileType == FileType.REGULAR_FILE && !file.isFile()) { + this.addError("File is not a regular file."); + return false; + } + + if (this.fileType == FileType.DIRECTORY && !file.isDirectory()) { + this.addError("File is not a directory."); + return false; + } + + return true; } @Override public File parseValues(@NotNull String @NotNull [] args) { File file = new File(args[0]); - if (this.needsToExist && !file.exists()) { - this.addError("File does not exist."); - return null; - } - return file; + return this.checkFile(file) ? file : null; } @Override public @Nullable String getDescription() { - return "A file path."; + return "A file path of" + + (this.mustExist ? " an existing " : " a ") + + this.fileType.getName(false) + + "."; + } + + @Override + public @Nullable TextFormatter getRepresentation() { + return new TextFormatter( + "path" + File.separator + "to" + File.separator + this.fileType.getName(true) + ); } } diff --git a/src/main/java/lanat/argumentTypes/FloatArgumentType.java b/src/main/java/lanat/argumentTypes/FloatArgumentType.java index 7a240f85..fa987fbd 100644 --- a/src/main/java/lanat/argumentTypes/FloatArgumentType.java +++ b/src/main/java/lanat/argumentTypes/FloatArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a floating point number. + * @see Float */ public class FloatArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java b/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java index 0c41a446..cd15a13a 100644 --- a/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java +++ b/src/main/java/lanat/argumentTypes/FromParseableArgumentType.java @@ -11,6 +11,7 @@ * 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. + * @see Parseable */ public class FromParseableArgumentType, TInner> extends ArgumentType { private final @NotNull T parseable; diff --git a/src/main/java/lanat/argumentTypes/IntegerArgumentType.java b/src/main/java/lanat/argumentTypes/IntegerArgumentType.java index 52637a41..4736711b 100644 --- a/src/main/java/lanat/argumentTypes/IntegerArgumentType.java +++ b/src/main/java/lanat/argumentTypes/IntegerArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes an integer number. + * @see Integer */ public class IntegerArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java b/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java index bcb650ea..caf095c8 100644 --- a/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java +++ b/src/main/java/lanat/argumentTypes/KeyValuesArgumentType.java @@ -18,6 +18,7 @@ *

* @param The type of the argument type used to parse the values. * @param The type of the values. + * @see HashMap */ public class KeyValuesArgumentType, Ts> extends ArgumentType> { private final @NotNull ArgumentType valueType; diff --git a/src/main/java/lanat/argumentTypes/LongArgumentType.java b/src/main/java/lanat/argumentTypes/LongArgumentType.java index bef388e8..25e4cc23 100644 --- a/src/main/java/lanat/argumentTypes/LongArgumentType.java +++ b/src/main/java/lanat/argumentTypes/LongArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a long integer number. + * @see Long */ public class LongArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/NumberArgumentType.java b/src/main/java/lanat/argumentTypes/NumberArgumentType.java index 0445ff3a..7f887f3d 100644 --- a/src/main/java/lanat/argumentTypes/NumberArgumentType.java +++ b/src/main/java/lanat/argumentTypes/NumberArgumentType.java @@ -10,6 +10,7 @@ * of {@link #parseValues(String[])} that will parse the first argument as a number using the * function returned by {@link #getParseFunction()}. * @param The type of number that this argument type is. + * @see Number */ public abstract class NumberArgumentType extends ArgumentType { /** diff --git a/src/main/java/lanat/argumentTypes/ShortArgumentType.java b/src/main/java/lanat/argumentTypes/ShortArgumentType.java index 529d419c..4ed67dda 100644 --- a/src/main/java/lanat/argumentTypes/ShortArgumentType.java +++ b/src/main/java/lanat/argumentTypes/ShortArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a short integer number. + * @see Short */ public class ShortArgumentType extends NumberArgumentType { @Override diff --git a/src/main/java/lanat/argumentTypes/StringArgumentType.java b/src/main/java/lanat/argumentTypes/StringArgumentType.java index 1f04a3c0..957a0b13 100644 --- a/src/main/java/lanat/argumentTypes/StringArgumentType.java +++ b/src/main/java/lanat/argumentTypes/StringArgumentType.java @@ -7,6 +7,7 @@ /** * An argument type that takes a string of characters. + * @see String */ public class StringArgumentType extends ArgumentType { @Override diff --git a/src/main/java/lanat/utils/UtlMisc.java b/src/main/java/lanat/utils/UtlMisc.java index 68c68899..f6775495 100644 --- a/src/main/java/lanat/utils/UtlMisc.java +++ b/src/main/java/lanat/utils/UtlMisc.java @@ -2,7 +2,6 @@ import lanat.CommandUser; import lanat.MultipleNamesAndDescription; -import lanat.exceptions.ObjectAlreadyExistsException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,7 +19,7 @@ private UtlMisc() {} */ public static void requireUniqueElements( @NotNull List list, - @NotNull Function exceptionSupplier + @NotNull Function exceptionSupplier ) { for (int i = 0; i < list.size(); i++) { final var el = list.get(i); @@ -63,8 +62,9 @@ boolean equalsByNamesAndParentCmd(@NotNull T a, @NotNull T b) { * @param defaultObj The function to apply to {@code obj} if it is not {@code null} * @return {@code null} if {@code obj} is {@code null}, otherwise returns the result of the given function * @param The type of the objects + * @param The type of the result of the function */ - public static @Nullable T nullOrElse(@Nullable T obj, @NotNull Function<@NotNull T, @NotNull T> defaultObj) { + public static @Nullable R nullOrElse(@Nullable T obj, @NotNull Function<@NotNull T, @NotNull R> defaultObj) { return obj == null ? null : defaultObj.apply(obj); } } diff --git a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java index f920fad8..c91fde58 100644 --- a/src/test/java/lanat/test/manualTests/CommandTemplateExample.java +++ b/src/test/java/lanat/test/manualTests/CommandTemplateExample.java @@ -4,12 +4,10 @@ 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 lanat.argumentTypes.*; import org.jetbrains.annotations.NotNull; +import java.io.File; import java.util.Optional; @Command.Define(names = "my-program", description = "This is a test program.") @@ -19,7 +17,7 @@ public CommandTemplateExample() {} @Argument.Define(argType = StringArgumentType.class, description = "This is a string argument.") public Optional string; - @Argument.Define(description = "", required = true) + @Argument.Define(description = "") public double number = 12; @Argument.Define(argType = StdinArgumentType.class) @@ -28,6 +26,8 @@ public CommandTemplateExample() {} @Argument.Define(names = "arg1", argType = StringArgumentType.class) public String arg1; + @Argument.Define(description = "") + public File file; @Argument.Define(names = "arg1a", argType = StringArgumentType.class) public String arg1copy; @@ -42,6 +42,20 @@ public CommandTemplateExample() {} public static void beforeInit(@NotNull CommandBuildHelper helper) { helper., Double>arg("number") .withArgType(new NumberRangeArgumentType<>(5.5, 15.89)); + helper.arg("file") + .withArgType(new FileArgumentType(true, FileArgumentType.FileType.REGULAR_FILE) { + @Override + protected boolean checkFile(@NotNull File file) { + if (!super.checkFile(file)) return false; + + if (!file.canExecute()) { + this.addError("File is not executable."); + return false; + } + + return true; + } + }); } @InitDef diff --git a/src/test/java/lanat/test/manualTests/ManualTests.java b/src/test/java/lanat/test/manualTests/ManualTests.java index 3bcfb21f..783de0b5 100644 --- a/src/test/java/lanat/test/manualTests/ManualTests.java +++ b/src/test/java/lanat/test/manualTests/ManualTests.java @@ -10,7 +10,7 @@ public final class ManualTests { @Test public void main() { - String input = "--help --version sub-command -h"; + String input = " "; // write some stuff to stdin System.setIn(new ByteArrayInputStream("hello world\ngoodbye".getBytes())); From d766754abd898f7a471014c336453c7abf8a39ac Mon Sep 17 00:00:00 2001 From: DarviL Date: Sun, 22 Oct 2023 21:04:56 +0200 Subject: [PATCH 14/15] fix: tests not running --- src/test/java/lanat/test/manualTests/ManualTests.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/java/lanat/test/manualTests/ManualTests.java b/src/test/java/lanat/test/manualTests/ManualTests.java index 783de0b5..54516626 100644 --- a/src/test/java/lanat/test/manualTests/ManualTests.java +++ b/src/test/java/lanat/test/manualTests/ManualTests.java @@ -15,7 +15,13 @@ public void main() { // write some stuff to stdin System.setIn(new ByteArrayInputStream("hello world\ngoodbye".getBytes())); - var parsed = ArgumentParser.parseFromInto(CommandTemplateExample.class, CLInput.from(input)); + var parsed = ArgumentParser.parseFromInto( + CommandTemplateExample.class, + CLInput.from(input), + o -> o.exitIfErrors() + .printErrors() + .printHelpIfNoInput() + ); parsed.string .ifPresentOrElse( From 3c49edc1e94c163ff6301ca80c36d12039d8404f Mon Sep 17 00:00:00 2001 From: DarviL Date: Sun, 22 Oct 2023 21:16:39 +0200 Subject: [PATCH 15/15] fix incorrect assert comment --- src/main/java/lanat/ArgumentTypeInfer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/lanat/ArgumentTypeInfer.java b/src/main/java/lanat/ArgumentTypeInfer.java index 7942c97f..cb9d88b4 100644 --- a/src/main/java/lanat/ArgumentTypeInfer.java +++ b/src/main/java/lanat/ArgumentTypeInfer.java @@ -69,7 +69,7 @@ void registerNumericWithTuple( @NotNull Class infer ) { assert !infer.isPrimitive() && inferPrimitive.isPrimitive() - : "Infer must be a primitive type and inferPrimitive must be a non-primitive type."; + : "Infer must be a non-primitive type and inferPrimitive must be a primitive type."; // register both the primitive and non-primitive types ArgumentTypeInfer.register(type, inferPrimitive, infer);