From 45b8f372faaf963a1630b137c41b9e5e3c2a868e Mon Sep 17 00:00:00 2001 From: darvil82 Date: Sat, 25 May 2024 14:01:41 +0200 Subject: [PATCH 1/7] improve error and help message for SingleValueListArgumentType --- .../argumentTypes/SingleValueListArgumentType.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java b/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java index 86a732304..46eec563f 100644 --- a/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java +++ b/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java @@ -75,7 +75,7 @@ public T parseValues(@NotNull String @NotNull [] values) { return this.listValues[i]; } - this.addError("Invalid value: '" + values[0] + "'."); + this.addError("Value '" + values[0] + "' not matching any in " + this.getRepresentation()); return null; } @@ -105,9 +105,16 @@ public T parseValues(@NotNull String @NotNull [] values) { @Override public @Nullable String getDescription() { - return "Specify one of the following values: " + var initialValue = this.getInitialValue(); + + return "Specify one of the values in " + String.join(", ", Stream.of(this.listValuesStr).toList()) - + (this.getInitialValue() == null ? "" : (". Default is " + this.getInitialValue())) + + ( + initialValue == null + ? "" + : (". Default is " + TextFormatter.of(this.valueToString(initialValue), SimpleColor.YELLOW) + .addFormat(FormatOption.BOLD)) + ) + "."; } } \ No newline at end of file From b7895a571160fef75a02eec041fa9d9bdc10958d Mon Sep 17 00:00:00 2001 From: darvil82 Date: Sat, 25 May 2024 15:05:13 +0200 Subject: [PATCH 2/7] fix: SingleValueListArgumentType allowing duplicates and empty values feat: Improve EnumArgumentType to use annotations to specify defaults/names --- src/main/java/lanat/ArgumentType.java | 8 +++ .../lanat/argumentTypes/EnumArgumentType.java | 56 ++++++++++++++++--- .../SingleValueListArgumentType.java | 12 +++- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/main/java/lanat/ArgumentType.java b/src/main/java/lanat/ArgumentType.java index 680c3538a..51153c690 100644 --- a/src/main/java/lanat/ArgumentType.java +++ b/src/main/java/lanat/ArgumentType.java @@ -186,6 +186,14 @@ public T getFinalValue() { return this.getValue(); // by default, the final value is just the current value. subclasses can override this. } + /** + * Sets the initial value of this argument type. + * @param initialValue The initial value of this argument type. + */ + public void setInitialValue(T initialValue) { + this.initialValue = initialValue; + } + /** * Returns the initial value of this argument type, if specified. * @return The initial value of this argument type, {@code null} if not specified. diff --git a/src/main/java/lanat/argumentTypes/EnumArgumentType.java b/src/main/java/lanat/argumentTypes/EnumArgumentType.java index 6baebcd88..97c8164b3 100644 --- a/src/main/java/lanat/argumentTypes/EnumArgumentType.java +++ b/src/main/java/lanat/argumentTypes/EnumArgumentType.java @@ -2,6 +2,13 @@ import org.jetbrains.annotations.NotNull; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.Optional; + /** * An argument type that takes a valid enum value. *

@@ -13,23 +20,56 @@ public class EnumArgumentType> extends SingleValueListArgumentType { /** * 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. + * @param clazz The class of the enum type to use. */ - public EnumArgumentType(@NotNull T defaultValue) { - super(defaultValue.getDeclaringClass().getEnumConstants(), defaultValue); + public EnumArgumentType(@NotNull Class clazz) { + super(clazz.getEnumConstants()); + this.setDefault(clazz); } /** - * Creates a new enum argument type. + * Sets the default value of the enum type by using the {@link Default} annotation. * @param clazz The class of the enum type to use. */ - public EnumArgumentType(@NotNull Class clazz) { - super(clazz.getEnumConstants()); - } + private void setDefault(@NotNull Class clazz) { + var defaultFields = Arrays.stream(clazz.getDeclaredFields()) + .filter(f -> f.isAnnotationPresent(Default.class)) + .toList(); + + if (defaultFields.isEmpty()) + return; + if (defaultFields.size() > 1) + throw new IllegalArgumentException("Only one default value can be set."); + + this.setInitialValue( + Arrays.stream(this.listValues) + .filter(v -> v.name().equals(defaultFields.get(0).getName())) + .findFirst() + .orElseThrow() + ); + } @Override protected @NotNull String valueToString(@NotNull T value) { - return value.name(); + try { + return Optional.ofNullable(value.getClass().getField(value.name()).getAnnotation(WithName.class)) + .map(WithName::value) + .orElseGet(value::name); + } catch (NoSuchFieldException e) { + return value.name(); + } } + + /** An annotation that specifies the name the user will have to write to select this value. */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface WithName { + String value(); + } + + /** An annotation that specifies the default value of the enum type. */ + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface Default { } } \ No newline at end of file diff --git a/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java b/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java index 46eec563f..fe04c9a93 100644 --- a/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java +++ b/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java @@ -1,6 +1,7 @@ package lanat.argumentTypes; import lanat.ArgumentType; +import lanat.utils.UtlMisc; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import textFormatter.FormatOption; @@ -47,14 +48,21 @@ protected SingleValueListArgumentType(@NotNull T @NotNull [] listValues) { if (this.listValues.length == 0) throw new IllegalArgumentException("The list of values cannot be empty."); - return Stream.of(this.listValues) + var sanitized = Stream.of(this.listValues) .map(this::valueToString) .map(String::trim) .peek(v -> { + if (v.isEmpty()) + throw new IllegalArgumentException("Value cannot be empty."); + if (v.chars().anyMatch(Character::isWhitespace)) throw new IllegalArgumentException("Value cannot contain spaces: '" + v + "'."); }) - .toArray(String[]::new); + .toList(); + + UtlMisc.requireUniqueElements(sanitized, e -> new IllegalArgumentException("Duplicate value: '" + e + "'.")); + + return sanitized.toArray(String[]::new); } /** From b37cf59aa3459ab8dffa541591712206e5f2aa44 Mon Sep 17 00:00:00 2001 From: darvil82 Date: Sat, 25 May 2024 17:01:16 +0200 Subject: [PATCH 3/7] feat: Enums can now be inferred for argument types. feat: ArgumentTypeInfer can now add predicate infers. Slower to lookup, but allow for more precise control --- src/main/java/lanat/ArgumentTypeInfer.java | 80 +++++++++++++++++-- .../lanat/test/units/TestArgumentTypes.java | 19 +++-- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/main/java/lanat/ArgumentTypeInfer.java b/src/main/java/lanat/ArgumentTypeInfer.java index 3f19f2931..19a9de557 100644 --- a/src/main/java/lanat/ArgumentTypeInfer.java +++ b/src/main/java/lanat/ArgumentTypeInfer.java @@ -7,8 +7,9 @@ import utils.exceptions.DisallowedInstantiationException; import java.io.File; -import java.util.HashMap; -import java.util.Optional; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; /** @@ -34,12 +35,32 @@ private ArgumentTypeInfer() { throw new DisallowedInstantiationException(ArgumentTypeInfer.class); } + /** + * A predicate that checks if the argument type should be inferred for the specified type. + */ + public record PredicateInfer( + Predicate> predicate, + Function, ? extends ArgumentType> supplier, + @NotNull String name + ) { + boolean matches(@NotNull Class clazz) { + return this.predicate.test(clazz); + } + + @SuppressWarnings("unchecked") + @NotNull ArgumentType apply(@NotNull Class clazz) { + return (ArgumentType)this.supplier.apply((Class)clazz); + } + } + /** * Mapping of types to their corresponding argument types. Used for inferring. * Argument types are stored as suppliers so that we have no shared references. * */ private static final HashMap, Supplier>> INFER_ARGUMENT_TYPES_MAP = new HashMap<>(); + private static final List> PREDICATE_INFERS = new ArrayList<>(5); + /** The default range to use for argument types that accept multiple values. */ public static final Range DEFAULT_TYPE_RANGE = Range.AT_LEAST_ONE; @@ -61,6 +82,18 @@ public static void register(@NotNull Supplier> type, @ ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.put(clazz, type); } + /** + * Registers an argument type to be inferred for the specified type, if the predicate is true. + * The predicate will be called each time any type is required to be inferred. + * @param predicateInfer The predicate to check if the argument type should be inferred. + */ + public static void register(@NotNull ArgumentTypeInfer.PredicateInfer predicateInfer) { + if (ArgumentTypeInfer.PREDICATE_INFERS.stream().anyMatch(c -> c.name().equals(predicateInfer.name()))) + throw new IllegalArgumentException("Predicate infer already registered with name: " + predicateInfer.name()); + + ArgumentTypeInfer.PREDICATE_INFERS.add(predicateInfer); + } + /** * Registers an argument type to be inferred for the specified type, including the primitive form. * @param type The argument type to infer. @@ -90,6 +123,26 @@ public static void unregister(@NotNull Class clazz) { ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.remove(clazz); } + /** + * Removes the {@link PredicateInfer} with the specified name. + * @param name The name of the predicate infer to remove. + * @throws IllegalArgumentException If no predicate infer is found with the specified name. + */ + public static void unregister(@NotNull String name) { + if (ArgumentTypeInfer.PREDICATE_INFERS.removeIf(c -> c.name().equals(name))) + return; + + throw new IllegalArgumentException("No predicate infer registered with name: " + name); + } + + /** + * Returns a list of all the predicate infers that are registered. + * @return An unmodifiable list of all the predicate infers that are registered. + */ + public static List> getPredicateInfers() { + return Collections.unmodifiableList(ArgumentTypeInfer.PREDICATE_INFERS); + } + /** * Removes the argument type inference for the specified type, including the primitive form. * @param boxed The boxed type to unregister the argument type from. @@ -112,9 +165,20 @@ public static void unregisterWithPrimitive( * @throws ArgumentTypeInferException If no argument type is found for the specified type. */ public static @NotNull ArgumentType get(@NotNull Class clazz) { - return Optional.ofNullable(ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.get(clazz)) - .map(Supplier::get) - .orElseThrow(() -> new ArgumentTypeInferException(clazz)); + var infer = Optional.ofNullable(ArgumentTypeInfer.INFER_ARGUMENT_TYPES_MAP.get(clazz)) + .map(Supplier::get); + + if (infer.isPresent()) + return infer.get(); + + var predicateInfer = ArgumentTypeInfer.PREDICATE_INFERS.stream() + .filter(c -> c.matches(clazz)) + .findFirst(); + + if (predicateInfer.isPresent()) + return predicateInfer.get().apply(clazz); + + throw new ArgumentTypeInferException(clazz); } @@ -163,6 +227,7 @@ void registerWithTuple( registerWithPrimitive(BooleanArgumentType::new, Boolean.class, boolean.class); register(() -> new FileArgumentType(false), File.class); + setDefaultPredicateInfers(); registerWithTuple(IntegerArgumentType::new, Integer.class, int.class); registerWithTuple(FloatArgumentType::new, Float.class, float.class); @@ -171,4 +236,9 @@ void registerWithTuple( registerWithTuple(ShortArgumentType::new, Short.class, short.class); registerWithTuple(ByteArgumentType::new, Byte.class, byte.class); } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void setDefaultPredicateInfers() { + register(new PredicateInfer<>(Enum.class::isAssignableFrom, c -> new EnumArgumentType(c), "EnumArgumentType")); + } } \ No newline at end of file diff --git a/src/test/java/lanat/test/units/TestArgumentTypes.java b/src/test/java/lanat/test/units/TestArgumentTypes.java index 5cf6b6291..b4f725fd1 100644 --- a/src/test/java/lanat/test/units/TestArgumentTypes.java +++ b/src/test/java/lanat/test/units/TestArgumentTypes.java @@ -15,7 +15,16 @@ public class TestArgumentTypes extends UnitTests { private enum TestEnum { - ONE, TWO, THREE + ONE, + @EnumArgumentType.Default + TWO, + THREE + } + + private enum TestEnum2 { + ONE, + TWO, + THREE } @Override @@ -31,8 +40,8 @@ protected TestingParser setParser() { .defaultValue(new Integer[] { 10101 }) ); this.addArgument(Argument.create(new FileArgumentType(true), "file")); - this.addArgument(Argument.create(new EnumArgumentType<>(TestEnum.TWO), "enum")); - this.addArgument(Argument.create(new EnumArgumentType<>(TestEnum.class), "enum2")); + this.addArgument(Argument.create(new EnumArgumentType<>(TestEnum.class), "enum")); + this.addArgument(Argument.create(new EnumArgumentType<>(TestEnum2.class), "enum2")); this.addArgument(Argument.create(new OptListArgumentType(List.of("foo", "bar", "qux"), "qux"), "optlist")); this.addArgument(Argument.create(new OptListArgumentType("foo", "bar", "qux"), "optlist2")); this.addArgument(Argument.create(new KeyValuesArgumentType<>(new IntegerArgumentType()), "key-value")); @@ -107,8 +116,8 @@ public void testEnum() { assertEquals(TestEnum.TWO, this.parser.parseGetValues("").get("enum").orElse(null)); // default value // test without default value - assertEquals(TestEnum.ONE, this.parseArg("enum2", "ONE")); - assertEquals(TestEnum.TWO, this.parseArg("enum2", "TWO")); + assertEquals(TestEnum2.ONE, this.parseArg("enum2", "ONE")); + assertEquals(TestEnum2.TWO, this.parseArg("enum2", "TWO")); this.assertNotPresent("enum2"); } From 327a0764a3599a9ab57e5972fafe02215c6e30a4 Mon Sep 17 00:00:00 2001 From: darvil82 Date: Sat, 25 May 2024 17:05:24 +0200 Subject: [PATCH 4/7] remove unnecesary code from readme --- README.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 88984cf7e..71bb786cd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

-### Example +## Example - First, we define our Command by creating a *Command Template*. ```java @@ -28,17 +28,9 @@ @Argument.Define(names = {"age", "a"}, description = "The age of the user.", prefix = Argument.Prefix.PLUS) public int age = 18; - - @InitDef - public static void beforeInit(@NotNull CommandBuildContext ctx) { - // configure the argument "age" to have an argument type of - // number range and set the range to 1-100 - ctx.argWithType("age", new NumberRangeArgumentType<>(1, 100)) - .onOk(v -> System.out.println("The age is valid!")); - } } ``` - + - Then, let that class definition also serve as the container for the parsed values. ```java class Test { From 2fe64da2c185ecdf16a68d97dd5df71198555ac7 Mon Sep 17 00:00:00 2001 From: darvil82 Date: Sat, 25 May 2024 17:10:19 +0200 Subject: [PATCH 5/7] fix description of SingleValueListArgumentType --- .../java/lanat/argumentTypes/SingleValueListArgumentType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java b/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java index fe04c9a93..26cd6ee08 100644 --- a/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java +++ b/src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java @@ -115,7 +115,7 @@ public T parseValues(@NotNull String @NotNull [] values) { public @Nullable String getDescription() { var initialValue = this.getInitialValue(); - return "Specify one of the values in " + return "Specify one of the following values: " + String.join(", ", Stream.of(this.listValuesStr).toList()) + ( initialValue == null From f0b7b3081c0111e271c154782c14c5185bdaa67a Mon Sep 17 00:00:00 2001 From: darvil82 Date: Sat, 25 May 2024 17:35:34 +0200 Subject: [PATCH 6/7] improve predicate register overload in ArgumentTypeInfer --- src/main/java/lanat/ArgumentTypeInfer.java | 22 ++++++++++++------- .../helpRepresentation/HelpFormatter.java | 2 +- .../lanat/helpRepresentation/LayoutItem.java | 8 +++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/java/lanat/ArgumentTypeInfer.java b/src/main/java/lanat/ArgumentTypeInfer.java index 19a9de557..61be0589f 100644 --- a/src/main/java/lanat/ArgumentTypeInfer.java +++ b/src/main/java/lanat/ArgumentTypeInfer.java @@ -40,7 +40,7 @@ private ArgumentTypeInfer() { */ public record PredicateInfer( Predicate> predicate, - Function, ? extends ArgumentType> supplier, + Function, ? extends ArgumentType> typeSupplier, @NotNull String name ) { boolean matches(@NotNull Class clazz) { @@ -49,7 +49,7 @@ boolean matches(@NotNull Class clazz) { @SuppressWarnings("unchecked") @NotNull ArgumentType apply(@NotNull Class clazz) { - return (ArgumentType)this.supplier.apply((Class)clazz); + return (ArgumentType)this.typeSupplier.apply((Class)clazz); } } @@ -85,13 +85,19 @@ public static void register(@NotNull Supplier> type, @ /** * Registers an argument type to be inferred for the specified type, if the predicate is true. * The predicate will be called each time any type is required to be inferred. - * @param predicateInfer The predicate to check if the argument type should be inferred. + * @param predicate The predicate to check if the argument type should be inferred. + * @param typeSupplier The argument type to infer. + * @param name The name of the predicate infer. */ - public static void register(@NotNull ArgumentTypeInfer.PredicateInfer predicateInfer) { - if (ArgumentTypeInfer.PREDICATE_INFERS.stream().anyMatch(c -> c.name().equals(predicateInfer.name()))) - throw new IllegalArgumentException("Predicate infer already registered with name: " + predicateInfer.name()); + public static void register( + @NotNull Predicate> predicate, + @NotNull Function, ? extends ArgumentType> typeSupplier, + @NotNull String name + ) { + if (ArgumentTypeInfer.PREDICATE_INFERS.stream().anyMatch(c -> c.name().equals(name))) + throw new IllegalArgumentException("Predicate infer already registered with name: " + name); - ArgumentTypeInfer.PREDICATE_INFERS.add(predicateInfer); + ArgumentTypeInfer.PREDICATE_INFERS.add(new PredicateInfer<>(predicate, typeSupplier, name)); } /** @@ -239,6 +245,6 @@ void registerWithTuple( @SuppressWarnings({"rawtypes", "unchecked"}) private static void setDefaultPredicateInfers() { - register(new PredicateInfer<>(Enum.class::isAssignableFrom, c -> new EnumArgumentType(c), "EnumArgumentType")); + register(Enum.class::isAssignableFrom, c -> new EnumArgumentType(c), "EnumArgumentType"); } } \ No newline at end of file diff --git a/src/main/java/lanat/helpRepresentation/HelpFormatter.java b/src/main/java/lanat/helpRepresentation/HelpFormatter.java index 4e2bcac65..c15fede27 100644 --- a/src/main/java/lanat/helpRepresentation/HelpFormatter.java +++ b/src/main/java/lanat/helpRepresentation/HelpFormatter.java @@ -198,7 +198,7 @@ public final void removeLayoutItems(int... indices) { final var buffer = new StringBuilder(); for (int i = 0; i < this.layout.size(); i++) { - final var generatedContent = this.layout.get(i).generate(this, cmd); + final var generatedContent = this.layout.get(i).generate(cmd); if (generatedContent == null) continue; diff --git a/src/main/java/lanat/helpRepresentation/LayoutItem.java b/src/main/java/lanat/helpRepresentation/LayoutItem.java index cabc71ab6..d16711084 100644 --- a/src/main/java/lanat/helpRepresentation/LayoutItem.java +++ b/src/main/java/lanat/helpRepresentation/LayoutItem.java @@ -39,7 +39,7 @@ public static LayoutItem of(@NotNull Function<@NotNull Command, @Nullable String /** * Creates a new {@link LayoutItem} with the given {@link Supplier} that generates a {@link String}. * - * @param layoutGenerator the supplier that generates the content of the layout item + * @param layoutGenerator the typeSupplier that generates the content of the layout item * @return the new LayoutItem */ public static LayoutItem of(@NotNull Supplier<@Nullable String> layoutGenerator) { @@ -140,18 +140,16 @@ public LayoutItem withTitle(String title) { /** * Generates the content of the layout item. The reason this method requires a {@link HelpFormatter} is because it * provides the indent size and the parent command. - * - * @param helpFormatter the help formatter that is generating the help message * @return the content of the layout item */ - public @Nullable String generate(@NotNull HelpFormatter helpFormatter, @NotNull Command cmd) { + public @Nullable String generate(@NotNull Command cmd) { final var content = this.generator.apply(cmd); return (content == null || content.isEmpty()) ? null : ( System.lineSeparator().repeat(this.marginTop) + (this.title == null ? "" : this.title + System.lineSeparator().repeat(2)) // strip() is used here because trim() also removes \022 (escape character) - + UtlString.indent(content.strip(), this.indentCount * helpFormatter.getIndentSize()) + + UtlString.indent(content.strip(), this.indentCount * HelpFormatter.getIndentSize()) + System.lineSeparator().repeat(this.marginBottom) ); } From 2bbfb42394ac0f3f30f10fb4eb57d263b694f7fa Mon Sep 17 00:00:00 2001 From: darvil82 Date: Sat, 25 May 2024 17:50:49 +0200 Subject: [PATCH 7/7] fixed README --- README.md | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 71bb786cd..1554c3f4a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Example - First, we define our Command by creating a *Command Template*. - + ```java @Command.Define class MyProgram { @@ -28,30 +28,39 @@ @Argument.Define(names = {"age", "a"}, description = "The age of the user.", prefix = Argument.Prefix.PLUS) public int age = 18; + + @InitDef + public static void beforeInit(@NotNull CommandBuildContext ctx) { + // configure the argument "age" to have an argument type of + // number range and set the range to 1-100 + ctx.argWithType("age", new NumberRangeArgumentType<>(18, 100)) + .onOk(v -> System.out.println("The age is valid!")); + } } ``` - - Then, let that class definition also serve as the container for the parsed values. +- Then, let that class definition also serve as the container for the parsed values. + ```java - class Test { - public static void main(String[] args) { - // example: david +a20 - var myProgram = ArgumentParser.parseFromInto(MyProgram.class, args); - - System.out.printf( - "Welcome %s! You are %d years old.%n", - myProgram.name, myProgram.age - ); - - // if no surname was specified, we'll show "none" instead - System.out.printf("The surname of the user is %s.%n", myProgram.surname.orElse("none")); - } + public static void main(String[] args) { + // example: david +a20 + var myProgram = ArgumentParser.parseFromInto(MyProgram.class, args); + + System.out.printf( + "Welcome %s! You are %d years old.%n", + myProgram.name, myProgram.age + ); + + // if no surname was specified, we'll show "none" instead + System.out.printf("The surname of the user is %s.%n", myProgram.surname.orElse("none")); } - ``` + ``` ## Documentation +Check out the [website](https://darvil82.github.io/lanat-web/) for more information. + [Click here](https://darvil82.github.io/lanat-docs/acquire-lanat.html) to get started with Lanat, and to check out the full documentation of the latest stable version.