Skip to content

Commit

Permalink
Merge pull request #26 from darvil82/dev
Browse files Browse the repository at this point in the history
Version 1.1.0
  • Loading branch information
darvil82 authored May 25, 2024
2 parents f5149d5 + 2bbfb42 commit 8e046d3
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 47 deletions.
39 changes: 20 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

<br><br>

### Example
## Example
- First, we define our Command by creating a *Command Template*.

```java
@Command.Define
class MyProgram {
Expand All @@ -33,33 +33,34 @@
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))
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.

Expand Down
8 changes: 8 additions & 0 deletions src/main/java/lanat/ArgumentType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
86 changes: 81 additions & 5 deletions src/main/java/lanat/ArgumentTypeInfer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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<T>(
Predicate<Class<?>> predicate,
Function<Class<T>, ? extends ArgumentType<?>> typeSupplier,
@NotNull String name
) {
boolean matches(@NotNull Class<?> clazz) {
return this.predicate.test(clazz);
}

@SuppressWarnings("unchecked")
@NotNull ArgumentType<T> apply(@NotNull Class<?> clazz) {
return (ArgumentType<T>)this.typeSupplier.apply((Class<T>)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<Class<?>, Supplier<? extends ArgumentType<?>>> INFER_ARGUMENT_TYPES_MAP = new HashMap<>();

private static final List<PredicateInfer<?>> 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;

Expand All @@ -61,6 +82,24 @@ public static void register(@NotNull Supplier<? extends ArgumentType<?>> 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 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 <T> void register(
@NotNull Predicate<Class<?>> predicate,
@NotNull Function<Class<T>, ? 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(new PredicateInfer<>(predicate, typeSupplier, name));
}

/**
* Registers an argument type to be inferred for the specified type, including the primitive form.
* @param type The argument type to infer.
Expand Down Expand Up @@ -90,6 +129,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<PredicateInfer<?>> 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.
Expand All @@ -112,9 +171,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);
}


Expand Down Expand Up @@ -163,6 +233,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);
Expand All @@ -171,4 +242,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(Enum.class::isAssignableFrom, c -> new EnumArgumentType(c), "EnumArgumentType");
}
}
56 changes: 48 additions & 8 deletions src/main/java/lanat/argumentTypes/EnumArgumentType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
Expand All @@ -13,23 +20,56 @@
public class EnumArgumentType<T extends Enum<T>> extends SingleValueListArgumentType<T> {
/**
* 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<T> 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<T> clazz) {
super(clazz.getEnumConstants());
}
private void setDefault(@NotNull Class<T> 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 { }
}
23 changes: 19 additions & 4 deletions src/main/java/lanat/argumentTypes/SingleValueListArgumentType.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -75,7 +83,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;
}

Expand Down Expand Up @@ -105,9 +113,16 @@ public T parseValues(@NotNull String @NotNull [] values) {

@Override
public @Nullable String getDescription() {
var initialValue = this.getInitialValue();

return "Specify one of the following values: "
+ 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))
)
+ ".";
}
}
2 changes: 1 addition & 1 deletion src/main/java/lanat/helpRepresentation/HelpFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/lanat/helpRepresentation/LayoutItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
);
}
Expand Down
Loading

0 comments on commit 8e046d3

Please sign in to comment.