diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java index ae9e4dce..126d8d85 100644 --- a/src/main/java/carpet/CarpetServer.java +++ b/src/main/java/carpet/CarpetServer.java @@ -82,6 +82,7 @@ public class CarpetServer implements ClientModInitializer,DedicatedServerModInit settingsManager.parseSettingsClass(CarpetSettings.class); extensions.forEach(CarpetExtension::onGameStarted); FabricAPIHooks.initialize(); + CarpetScriptServer.parseFunctionClasses(); } public static void onServerLoaded(MinecraftServer server) diff --git a/src/main/java/carpet/script/CarpetExpression.java b/src/main/java/carpet/script/CarpetExpression.java index 94c3cf10..67496d99 100644 --- a/src/main/java/carpet/script/CarpetExpression.java +++ b/src/main/java/carpet/script/CarpetExpression.java @@ -1,6 +1,7 @@ package carpet.script; import carpet.CarpetServer; +import carpet.script.annotation.AnnotationParser; import carpet.script.api.Auxiliary; import carpet.script.api.BlockIterators; import carpet.script.api.Entities; @@ -45,6 +46,7 @@ public class CarpetExpression Threading.apply(this.expr); Scoreboards.apply(this.expr); Monitoring.apply(this.expr); + AnnotationParser.apply(this.expr); CarpetServer.extensions.forEach(e -> e.scarpetApi(this)); } diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java index 92185750..5f056e98 100644 --- a/src/main/java/carpet/script/CarpetScriptServer.java +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -1,12 +1,26 @@ package carpet.script; import carpet.CarpetSettings; +import carpet.script.annotation.AnnotationParser; +import carpet.script.api.Auxiliary; +import carpet.script.api.BlockIterators; +import carpet.script.api.Entities; +import carpet.script.api.Inventories; +import carpet.script.api.Scoreboards; +import carpet.script.api.WorldAccess; import carpet.script.bundled.BundledModule; import carpet.CarpetServer; import carpet.script.bundled.FileModule; import carpet.script.bundled.Module; import carpet.script.exception.IntegrityException; import carpet.script.exception.InvalidCallbackException; +import carpet.script.language.Arithmetic; +import carpet.script.language.ControlFlow; +import carpet.script.language.DataStructures; +import carpet.script.language.Functions; +import carpet.script.language.Loops; +import carpet.script.language.Sys; +import carpet.script.language.Threading; import carpet.script.value.FunctionValue; import carpet.script.value.Value; import carpet.utils.CarpetProfiler; @@ -481,4 +495,25 @@ public class CarpetScriptServer { modules.values().forEach(host -> host.addAppCommands(s -> {})); } + + public static void parseFunctionClasses() + { + // Language + AnnotationParser.parseFunctionClass(Arithmetic.class); + AnnotationParser.parseFunctionClass(ControlFlow.class); + AnnotationParser.parseFunctionClass(DataStructures.class); + AnnotationParser.parseFunctionClass(Functions.class); + AnnotationParser.parseFunctionClass(Loops.class); + AnnotationParser.parseFunctionClass(Sys.class); + AnnotationParser.parseFunctionClass(Threading.class); + + // API + AnnotationParser.parseFunctionClass(Auxiliary.class); + AnnotationParser.parseFunctionClass(BlockIterators.class); + AnnotationParser.parseFunctionClass(Entities.class); + AnnotationParser.parseFunctionClass(Inventories.class); + AnnotationParser.parseFunctionClass(Scoreboards.class); + AnnotationParser.parseFunctionClass(carpet.script.language.Threading.class); + AnnotationParser.parseFunctionClass(WorldAccess.class); + } } diff --git a/src/main/java/carpet/script/Fluff.java b/src/main/java/carpet/script/Fluff.java index d3d64281..26d281e5 100644 --- a/src/main/java/carpet/script/Fluff.java +++ b/src/main/java/carpet/script/Fluff.java @@ -125,15 +125,7 @@ public abstract class Fluff public List unpackArgs(List lzargs, Context c, Context.Type contextType) { - List args = new ArrayList<>(); - for (LazyValue lv : lzargs) - { - Value arg = lv.evalValue(c, contextType); - if (arg instanceof FunctionUnpackedArgumentsValue) - args.addAll(((ListValue) arg).getItems()); - else - args.add(arg); - } + List args = unpackLazy(lzargs, c, contextType); if (!numParamsVaries() && getNumParams() != args.size()) throw new InternalExpressionException("Function " + getName() + " expected " + getNumParams() + " parameters, got " + args.size()); return args; diff --git a/src/main/java/carpet/script/annotation/AnnotationParser.java b/src/main/java/carpet/script/annotation/AnnotationParser.java new file mode 100644 index 00000000..aa605526 --- /dev/null +++ b/src/main/java/carpet/script/annotation/AnnotationParser.java @@ -0,0 +1,320 @@ +package carpet.script.annotation; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ClassUtils; + +import carpet.CarpetExtension; +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Fluff.AbstractLazyFunction; +import carpet.script.Fluff.TriFunction; +import carpet.script.exception.InternalExpressionException; +import carpet.script.LazyValue; +import carpet.script.value.Value; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +/** + *

This class parses methods annotated with the {@link ScarpetFunction} annotation in a given {@link Class}, generating + * fully-featured, automatically parsed and converted functions to be used in the Scarpet language.

+ * + *

This class and the rest in this package will try to ensure that the annotated method receives the proper parameters + * directly, without all the always-repeated code of evaluating {@link Value}s, checking and casting them to their respective + * types, and converting them to the final needed object.

+ * + *

To do that, functions will save a list of {@link ValueConverter}s to convert all their parameters. {@link ValueConverter}s + * are able to convert from any compatible {@link Value} instance into the requested parameter type, as long as they are registered + * using their respective {@code register} functions.

+ * + *

Built-in {@link ValueConverter}s include but are not limited to converters to convert {@link List}s to actual Java lists while also + * converting every item inside of the {@link List} to the specified generic parameter ({@code <>}), with the same applying for maps

+ * + *

Parameters can be given the annotations (present in the {@link Locator} and {@link Param} interfaces) in order to restrict them or + * make them more permissive to accept types, such as {@link Param.AllowSingleton} for lists.

+ * + *

You can also declare optional parameters by using Java's {@link Optional} as the type of one of your parameters, though it must be + * at the end of the function, just before varargs if present and/or any other {@link Optional} parameters.

+ * + *

Output of the annotated methods will also be converted to a compatible {@link LazyValue} using the registered {@link OutputConverter}s, + * allowing to remove the need of explicitly converting to a {@link Value} and then to a {@link LazyValue} just to end the method (though you can + * return a {@link LazyValue} if you want.

+ * + *

For a variable argument count, the Java varargs notation can be used in the last parameter, converting the function into a variable argument + * function that will pass all the rest of parameters to that last varargs parameter, also converted into the specified type.

+ * + *

To begin, use the {@link #parseFunctionClass(Class)} method in a class with methods annotated with the {@link ScarpetFunction} annotation.

+ * + * @see ScarpetFunction + * @see Locator.Block + * @see Locator.Vec3d + * @see Param.Strict + * @see Param.AllowSingleton + * @see Param.KeyValuePairs + * @see Param.Custom + * @see Optional + * @see OutputConverter#registerToValue(Class, java.util.function.Function) + * @see OutputConverter#register(Class, java.util.function.Function) + * @see ValueCaster#register(Class, String) + * @see SimpleTypeConverter#registerType(Class, Class, java.util.function.Function) + * @see Param.Params#registerStrictConverter(Class, boolean, ValueConverter) + * @see Param.Params#registerCustomConverterFactory(java.util.function.BiFunction) + */ +public final class AnnotationParser +{ + private static final List functionList = new ArrayList<>(); + + /** + *

Parses a given {@link Class} and registers its annotated methods, the ones with the {@link ScarpetFunction} annotation, + * to be used in the Scarpet language.

+ * + *

Only call this method once per class per lifetime of the JVM! (for example, at {@link CarpetExtension#onGameStarted()})

+ * + *

There is a set of requirements for the class and its methods:

+ *
    + *
  • Class must be concrete. That is, no interfaces or abstract classes should be passed
  • + *
  • Class must have the default constructor (or an equivalent) available. That is done in order to not need the {@code static} modifier in every method, making them faster to + * code and simpler to look at.
  • + *
  • Annotated methods must not be {@code static}. See claim above
  • + *
  • Annotated methods must not throw checked exceptions. They can throw regular {@link RuntimeException}s (including but not limited to + * {@link InternalExpressionException}). + * Basically, it's fine as long as you don't add a {@code throws} declaration to your methods.
  • + *
  • Varargs (or effectively varargs) annotated methods must explicitly declare a maximum number of parameters to ingest in the {@link ScarpetFunction} + * annotation. They can still declare an unlimited amount by setting that maximum to {@code -1}. "Effectively varargs" means a function that has + * a parameter using/requiring a {@link ValueConverter} that has declared {@link ValueConverter#consumesVariableArgs()}.
  • + *
  • Annotated methods must not have a parameter with generics as the varargs parameter. This is just because it was painful for me (altrisi) and + * didn't want to support it. Those will crash with a {@code ClassCastException}
  • + *
+ * + * @see ScarpetFunction + * @param The generic type of the class to parse. + * @param clazz The class to parse + */ + public static void parseFunctionClass(Class clazz) + { + if (Modifier.isAbstract(clazz.getModifiers())) + throw new IllegalArgumentException("Function class must be concrete! Class: " + clazz.getSimpleName()); + T instance; + try + { + instance = clazz.getConstructor().newInstance(); + } + catch (ReflectiveOperationException e) + { + throw new IllegalArgumentException( + "Couldn't create instance of given " + clazz + ". Make sure default constructor is available", e); + } + + Method[] methodz = clazz.getDeclaredMethods(); + for (Method method : methodz) + { + if (!method.isAnnotationPresent(ScarpetFunction.class)) + continue; + // Checks + if (Modifier.isStatic(method.getModifiers())) + throw new IllegalArgumentException("Annotated method '" + method.getName() + "', provided in '" + clazz + "' must not be static"); + if (method.getExceptionTypes().length != 0) + throw new IllegalArgumentException("Annotated method '" + method.getName() +"', provided in '"+clazz+"' must not declare checked exceptions"); + + ParsedFunction function = new ParsedFunction(method, instance); + functionList.add(function); + } + } + + /** + *

Adds all parsed functions to the given {@link Expression}.

+ *

This is handled automatically by Carpet

+ * + * @param expr The expression to add every function to + */ + public static void apply(Expression expr) + { + for (ParsedFunction function : functionList) + expr.addLazyFunction(function.name, function.scarpetParamCount, function); + } + + private static class ParsedFunction implements TriFunction, LazyValue> + { + private final String name; + private final boolean isMethodVarArgs; + private final int methodParamCount; + private final List> valueConverters; + private final Class varArgsType; + private final boolean primitiveVarArgs; + private final ValueConverter varArgsConverter; + private final OutputConverter outputConverter; + private final boolean isEffectivelyVarArgs; + private final int minParams; + private final int maxParams; + private final MethodHandle handle; + private final int scarpetParamCount; + private final Context.Type contextType; + + private ParsedFunction(final Method method, final Object instance) + { + this.name = method.getName(); + this.isMethodVarArgs = method.isVarArgs(); + this.methodParamCount = method.getParameterCount(); + + Parameter[] methodParameters = method.getParameters(); + this.valueConverters = new ObjectArrayList<>(); + for (int i = 0; i < this.methodParamCount; i++) + { + Parameter param = methodParameters[i]; + if (!isMethodVarArgs || i != this.methodParamCount - 1) // Varargs converter is separate + this.valueConverters.add(ValueConverter.fromAnnotatedType(param.getAnnotatedType())); + } + Class originalVarArgsType = isMethodVarArgs ? methodParameters[methodParamCount - 1].getType().getComponentType() : null; + this.varArgsType = ClassUtils.primitiveToWrapper(originalVarArgsType); // Primitive array cannot be cast to Obj[] + this.primitiveVarArgs = originalVarArgsType != null && originalVarArgsType.isPrimitive(); + this.varArgsConverter = isMethodVarArgs ? ValueConverter.fromAnnotatedType(methodParameters[methodParamCount - 1].getAnnotatedType()) : null; + @SuppressWarnings("unchecked") // Yes. Making a T is not worth + OutputConverter converter = OutputConverter.get((Class) method.getReturnType()); + this.outputConverter = converter; + + this.isEffectivelyVarArgs = isMethodVarArgs ? true : valueConverters.stream().anyMatch(ValueConverter::consumesVariableArgs); + this.minParams = valueConverters.stream().mapToInt(ValueConverter::valueConsumption).sum(); // Note: In !varargs, this is params + int maxParams = this.minParams; // Unlimited == Integer.MAX_VALUE + if (this.isEffectivelyVarArgs) + { + maxParams = method.getAnnotation(ScarpetFunction.class).maxParams(); + if (maxParams == -2) + throw new IllegalArgumentException("No maximum number of params specified for " + name + ", use -1 for unlimited. " + + "Provided in " + instance.getClass()); + if (maxParams < this.minParams) + throw new IllegalArgumentException("Provided maximum number of params for " + name + " is smaller than method's param count." + + "Provided in " + instance.getClass()); + if (maxParams == -1) + maxParams = Integer.MAX_VALUE; + } + this.maxParams = maxParams; + + // Why MethodHandles? + // MethodHandles are blazing fast (in some situations they can even compile to a single invokeVirtual), but slightly complex to work with. + // Their "polymorphic signature" makes them (by default) require the exact signature of the method and return type, in order to call the + // functions directly, not even accepting Objects. Therefore we change them to spreaders (accept array instead), return type of Object, + // and we also bind them to our instance. That makes them ever-so-slightly slower, since they have to cast the params, but it's not + // noticeable, and comparing the overhead that reflection's #invoke had over this, the difference is substantial + // (checking access at invoke vs create). + // Note: there is also MethodHandle#invoke and #invokeWithArguments, but those run #asType at every invocation, which is quite slow, so we + // are basically running it here. + try + { + MethodHandle tempHandle = MethodHandles.publicLookup().unreflect(method).asFixedArity().asSpreader(Object[].class, this.methodParamCount); + this.handle = tempHandle.asType(tempHandle.type().changeReturnType(Object.class)).bindTo(instance); + } catch (IllegalAccessException e) + { + throw new IllegalArgumentException(e); + } + + this.scarpetParamCount = this.isEffectivelyVarArgs ? -1 : this.minParams; + this.contextType = method.getAnnotation(ScarpetFunction.class).contextType(); + } + + @Override + public LazyValue apply(Context context, Context.Type t, List lazyValues) + { + List lv = AbstractLazyFunction.unpackLazy(lazyValues, context, contextType); + if (isEffectivelyVarArgs) + { + if (lv.size() < minParams) + throw new InternalExpressionException("Function '" + name + "' expected at least " + minParams + " arguments, got " + lv.size() + ". " + + getUsage()); + if (lv.size() > maxParams) + throw new InternalExpressionException("Function '" + name + " expected up to " + maxParams + " arguments, got " + lv.size() + ". " + + getUsage()); + } else + { + if (lv.size() != minParams) // min == max if not varargs. We do this cause we don't use function's arg checking to be able to return lazy + throw new InternalExpressionException("Function '" + name + "' expected " + minParams + " arguments, got " + lv.size() +". " + getUsage()); + } + Object[] params = getMethodParams(lv, context, t); + try + { + return outputConverter.convert(handle.invokeExact(params)); + } + catch (Throwable e) + { + if (e instanceof RuntimeException) + throw (RuntimeException) e; + throw (Error) e; // Stack overflow or something. Methods are guaranteed not to throw checked exceptions + } + } + + // Hot code: Must be optimized + private Object[] getMethodParams(final List lv, final Context context, Context.Type theLazyT) + { + Object[] params = new Object[methodParamCount]; + ListIterator lvIterator = lv.listIterator(); + + int regularArgs = isMethodVarArgs ? methodParamCount - 1 : methodParamCount; + for (int i = 0; i < regularArgs; i++) + { + params[i] = valueConverters.get(i).checkAndConvert(lvIterator, context, theLazyT); + if (params[i] == null) + throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage()); + } + if (isMethodVarArgs) + { + int remaining = lv.size() - lvIterator.nextIndex(); + Object[] varArgs; + if (varArgsConverter.consumesVariableArgs()) + { + List varArgsList = new ArrayList<>(); + while (lvIterator.hasNext()) + { + Object obj = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT); + if (obj == null) + throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage()); + varArgsList.add(obj); + } + varArgs = varArgsList.toArray(); + } else + { + varArgs = (Object[]) Array.newInstance(varArgsType, remaining / varArgsConverter.valueConsumption()); + for (int i = 0; lvIterator.hasNext(); i++) + { + varArgs[i] = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT); + if (varArgs[i] == null) + throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage()); + } + } + params[methodParamCount - 1] = primitiveVarArgs ? ArrayUtils.toPrimitive(varArgs) : varArgs; // Copies the array + } + return params; + } + + private String getUsage() + { + // Possibility: More descriptive messages using param.getName()? Would need changing gradle setup to keep those + StringBuilder builder = new StringBuilder("Usage: '"); + builder.append(name); + builder.append('('); + builder.append(valueConverters.stream().map(ValueConverter::getTypeName).filter(Objects::nonNull).collect(Collectors.joining(", "))); + if (varArgsConverter != null) + { + builder.append(", "); + builder.append(varArgsConverter.getTypeName()); + builder.append("s...)"); + } else + { + builder.append(')'); + } + builder.append("'"); + return builder.toString(); + } + } + + private AnnotationParser() {} +} diff --git a/src/main/java/carpet/script/annotation/FunctionsTestClass.java b/src/main/java/carpet/script/annotation/FunctionsTestClass.java new file mode 100644 index 00000000..8f81ff6c --- /dev/null +++ b/src/main/java/carpet/script/annotation/FunctionsTestClass.java @@ -0,0 +1,60 @@ +package carpet.script.annotation; + +import carpet.logging.HUDController; +import carpet.script.annotation.Param.AllowSingleton; +import carpet.script.exception.InternalExpressionException; +import net.minecraft.network.packet.s2c.play.TitleS2CPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.BaseText; +import net.minecraft.text.Text; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static net.minecraft.network.packet.s2c.play.TitleS2CPacket.Action; + +public class FunctionsTestClass { + @ScarpetFunction(maxParams = 6) + public Integer display_title2(@AllowSingleton List targets, String actionString, Optional content, Integer... times) { + TitleS2CPacket.Action action; + switch (actionString.toLowerCase(Locale.ROOT)) + { + case "title": + action = Action.TITLE; + break; + case "subtitle": + action = Action.SUBTITLE; + break; + case "actionbar": + action = Action.ACTIONBAR; + break; + case "clear": + action = Action.CLEAR; + break; + case "player_list_header": + case "player_list_footer": + action = null; + break; + default: + throw new InternalExpressionException("'display_title' requires 'title', 'subtitle', 'actionbar', 'player_list_header', 'player_list_footer' or 'clear' as second argument"); + } + if (action == null) // Player list + { + Map map = actionString.equals("player_list_header") ? HUDController.scarpet_headers : HUDController.scarpet_footers; + if (!content.isPresent()) // null + for (ServerPlayerEntity p : targets) + map.remove(p); + else + for (ServerPlayerEntity p : targets) + map.put(p, (BaseText) content.get()); + return targets.size(); + } + targets.forEach(p -> { + if (times.length == 3) p.networkHandler.sendPacket(new TitleS2CPacket(Action.TIMES, null, times[0], times[1], times[2])); + p.networkHandler.sendPacket(new TitleS2CPacket(action, content.orElse(null))); + }); + return targets.size(); + } +} diff --git a/src/main/java/carpet/script/annotation/ListConverter.java b/src/main/java/carpet/script/annotation/ListConverter.java new file mode 100644 index 00000000..5abf88ff --- /dev/null +++ b/src/main/java/carpet/script/annotation/ListConverter.java @@ -0,0 +1,90 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import carpet.script.value.ListValue; +import carpet.script.value.Value; + +/** + *

Converts a given {@link ListValue} into a {@link List} of values converted to {@code }.

+ * + *

If the {@link Param.AllowSingleton} annotation is specified, allows creating a singleton from + * a loose element compatible with the type conversion.

+ * + *

Lists provided by this converter are not linked to the initial list, and therefore will not + * reflect changes in either of them

+ * + * @param The type of the element that will be inside the list + */ +final class ListConverter implements ValueConverter> +{ + private final ValueConverter itemConverter; + private final boolean allowSingletonCreation; + + @Override + public String getTypeName() + { + return (allowSingletonCreation ? itemConverter.getTypeName() + " or " : "") + "list of " + itemConverter.getTypeName() + "s"; + } + + @Override + public List convert(Value value) + { + return value instanceof ListValue ? convertListValue((ListValue) value) : allowSingletonCreation ? convertSingleton(value) : null; + } + + private List convertListValue(ListValue values) + { + List list = new ArrayList<>(values.getItems().size()); + for (Value value : values) + { + T converted = itemConverter.convert(value); + if (converted == null) + return null; + list.add(converted); + } + return list; + } + + private List convertSingleton(Value val) + { + T converted = itemConverter.convert(val); + if (converted == null) + return null; + return Collections.singletonList(converted); + + } + + private ListConverter(AnnotatedType itemType, boolean allowSingletonCreation) + { + itemConverter = ValueConverter.fromAnnotatedType(itemType); + this.allowSingletonCreation = allowSingletonCreation; + } + + /** + *

Returns a new {@link ListConverter} to convert to the given {@link AnnotatedType}.

+ * + *

The returned {@link ValueConverter} will convert the objects inside the list to the + * generics specified in the {@link AnnotatedType}, and the {@link ValueConverter} will + * be set to accept non-list (but correct) items and make a singleton out of them + * if the {@link Param.AllowSingleton} annotation has been specified.

+ * + * @apiNote This method expects the {@link AnnotatedType} to already be of {@link List} type, and, while it will + * technically accept a non-{@link List} {@link AnnotatedType}, it will fail with an {@link ArrayIndexOutOfBoundsException} + * if it doesn't has at least one generic parameter. + * @param annotatedType The type to get generics information from + * @return A new {@link ListConverter} for the data specified in the {@link AnnotatedType} + */ + static ListConverter fromAnnotatedType(AnnotatedType annotatedType) + { + AnnotatedParameterizedType paramType = (AnnotatedParameterizedType) annotatedType; + AnnotatedType itemType = paramType.getAnnotatedActualTypeArguments()[0]; + boolean allowSingletonCreation = annotatedType.isAnnotationPresent(Param.AllowSingleton.class); + return new ListConverter<>(itemType, allowSingletonCreation); + } + +} diff --git a/src/main/java/carpet/script/annotation/Locator.java b/src/main/java/carpet/script/annotation/Locator.java new file mode 100644 index 00000000..f4d49345 --- /dev/null +++ b/src/main/java/carpet/script/annotation/Locator.java @@ -0,0 +1,270 @@ +package carpet.script.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; +import java.util.Iterator; + +import org.apache.commons.lang3.NotImplementedException; + +import carpet.script.Context; +import carpet.script.argument.Argument; +import carpet.script.argument.BlockArgument; +import carpet.script.argument.FunctionArgument; +import carpet.script.argument.Vector3Argument; +import carpet.script.bundled.Module; +import carpet.script.value.BlockValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.Value; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + *

Class that holds annotation for {@link Argument} locators to be used in Scarpet functions.

+ * + *

Note: Nothing in this class has been implemented at this point in time, since it requires Carpet changes. There is an implementation for + * Locator.Block working, but not implemented.

+ */ +public interface Locator +{ + /** + *

Represents that the annotated argument must be gotten by passing the arguments in there into a {@link BlockArgument} locator.

+ * + *

Must be used in either {@link BlockValue} or {@link BlockArgument} parameters

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @Deprecated // Not implemented, although implementation available + public @interface Block + { + /** + *

Whether or not should the locator accept a single {@link String} as the parameter and let parsing to {@link BlockValue}.

+ */ + boolean acceptString() default false; + + /** + *

Whether or not should the {@link BlockValue} argument be optional.

Requires the annotation to be present in a + * {@link BlockArgument} type, since it may return that the {@link BlockValue} is {@code null}, which would be considered as an incorrect + * type.

+ */ + boolean optional() default false; + + /** + *

Whether or not should the {@link BlockArgument} locator accept any string as the argument.

Requires the annotation to be present + * in a {@link BlockArgument} type, since it may just return a {@link String}

+ */ + boolean anyString() default false; + } + + /** + *

Represents that the annotated argument must be gotten by passing the arguments in there into a {@link Vector3Argument} locator.

+ * + *

Must be used in either a {@link Vector3Argument} or a {@link Vec3d} parameter, though the latest may not get all the data.

+ * + *

Note: This locator has not been implemented yet (TODO)

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @Deprecated // Not implemented + public @interface Vec3d + { + /** + *

Whether or not should the {@link Vector3Argument} locator accept an optional direction aside from the + * {@link net.minecraft.util.math.Vec3d}

This parameter can only be used in a {@link Vector3Argument} type, since else there is no way + * to get the direction too.

+ */ + boolean optionalDirection() default false; + + /** + *

Whether or not should the {@link Vector3Argument} locator accept an entity aside to get the {@link Vec3d} from and return that entity + * too

Note that you will only be able to get that entity if the annotation is present in a {@link Vector3Argument}

+ */ + boolean optionalEntity() default false; + } + + /** + *

Represents that the annotated argument must be gotten by passing the arguments in this annotation into a {@link FunctionArgument} + * locator

+ * + *

Can be used in both {@link FunctionArgument} and {@link FunctionValue} types, but the last won't have access to arguments provided to the + * function, even though they will still be consumed from the arguments the function was called with.

+ * + *

This will consume any remaining parameters passed to the function, therefore any other parameters after this will throw.

+ * + *

Note: This locator has not been implemented yet (TODO)

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @Deprecated // Not implemented + public @interface Function + { + /** + *

Whether this Locator should allow no function to be passed.

This is not compatible with {@link FunctionValue} type, since a + * converter returning {@code null} will throw as if the passed argument was incorrect. You can still use it when targeting + * {@link FunctionArgument}

+ */ + boolean allowNone() default false; + + /** + *

Whether the locator should check that the number of arguments passed along with the function matches the number of arguments that the + * located function requires. Note that FunctionLocators consume all remaining arguments even if this is set to {@code false}.

+ */ + boolean checkArgs(); + } + + /** + *

Class that holds locators and methods to get them

+ * + *

Not part of the public API, just that interfaces must have all members public

+ */ + static final class Locators + { + private Locators() {} + + static ValueConverter fromAnnotatedType(AnnotatedType annoType, Class type) + { + if (annoType.isAnnotationPresent(Block.class)) + return new BlockLocator(annoType.getAnnotation(Block.class), type); + if (annoType.isAnnotationPresent(Function.class)) + return new FunctionLocator(annoType.getAnnotation(Function.class), type); + if (annoType.isAnnotationPresent(Vec3d.class)) + return new Vec3dLocator(annoType.getAnnotation(Vec3d.class), type); + throw new IllegalStateException("Locator#fromAnnotatedType got called with an incompatible AnnotatedType"); + } + + private static class BlockLocator extends AbstractLocator + { + private final boolean returnBlockValue; + private final boolean acceptString; + private final boolean anyString; + private final boolean optional; + + public BlockLocator(Block annotation, Class type) + { + this.acceptString = annotation.acceptString(); + this.anyString = annotation.anyString(); + this.optional = annotation.optional(); + this.returnBlockValue = type == BlockValue.class; + if (returnBlockValue && (anyString || optional)) + throw new IllegalArgumentException("Can only use anyString or optional parameters of Locator.Block if targeting a BlockArgument"); + if (!returnBlockValue && type != BlockArgument.class) + throw new IllegalArgumentException("Locator.Block can only be used against BlockArgument or BlockValue types!"); + } + + @Override + public String getTypeName() + { + return "block"; + } + + @Override + public R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + BlockArgument locator = null; // BlockArgument.findIn((CarpetContext)context, valueIterator (requires changing to Value), 0, + // acceptString, optional, anyString); + // return (R) (returnBlockValue ? locator.block : locator); + throw new NotImplementedException("Locator.Block still requires adapting BlockArgument to accept iterators (which is actually simple)"); + } + } + + private static class Vec3dLocator extends AbstractLocator + { + private final boolean optionalDirection; + private final boolean optionalEntity; + private final boolean returnVec3d; + + public Vec3dLocator(Vec3d annotation, Class type) + { + this.optionalDirection = annotation.optionalDirection(); + this.optionalEntity = annotation.optionalEntity(); + this.returnVec3d = type == net.minecraft.util.math.Vec3d.class; // Because of the locator + if (returnVec3d && optionalDirection) + throw new IllegalArgumentException("optionalDirection Locator.Vec3d cannot be used for Vec3d type, use Vector3Argument instead"); + if (!returnVec3d && type != Vector3Argument.class) + throw new IllegalArgumentException("Locator.Vec3d can only be used in Vector3Argument or Vec3d types"); + } + + @Override + public String getTypeName() + { + return "position"; + } + + @Override + public R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + Vector3Argument locator = null; + // TODO Make the locator + // return (R) (returnVec3d ? locator.vec : locator); + throw new NotImplementedException("Locator.Vec3d still require adapting Vector3Argument to accept iterators!"); + } + } + + private static class FunctionLocator extends AbstractLocator + { + private final boolean returnFunctionValue; + private final boolean allowNone; + private final boolean checkArgs; + + FunctionLocator(Function annotation, Class type) + { + this.returnFunctionValue = type == FunctionValue.class; + if (!returnFunctionValue && type != FunctionArgument.class) + throw new IllegalArgumentException( + "Params annotated with Locator.Function must be of either FunctionArgument or FunctionValue type"); + this.allowNone = annotation.allowNone(); + this.checkArgs = annotation.checkArgs(); + if (returnFunctionValue && allowNone) + throw new IllegalArgumentException("Cannot use allowNone of Locator.Function in FunctionValue types, use FunctionArgument"); + } + + @Override + public R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + Module module = context.host.main; + FunctionArgument locator = null; + // TODO Make the locator + // return (R) (returnFunctionValue ? locator.function : locator); + throw new NotImplementedException("Locator.FunctionValue still requires adapting the annotation system to somehow get lv size" + + "or FunctionArgument to not depend on it!"); + } + + @Override + public String getTypeName() + { + return "function"; + } + } + + private static abstract class AbstractLocator implements ValueConverter, Locator + { + @Override + public R convert(Value value) + { + throw new UnsupportedOperationException("Cannot call a locator in a parameter that doesn't contain a context!"); + } + + @Override + public boolean consumesVariableArgs() + { + return true; + } + + @Override + public int valueConsumption() + { + return 1; + } + + @Override + public abstract R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT); + } + + } +} diff --git a/src/main/java/carpet/script/annotation/MapConverter.java b/src/main/java/carpet/script/annotation/MapConverter.java new file mode 100644 index 00000000..fd26d30f --- /dev/null +++ b/src/main/java/carpet/script/annotation/MapConverter.java @@ -0,0 +1,163 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import carpet.script.Context; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.Value; + +/** + *

Converts a {@link MapValue} to a {@link Map}, converting all of its contents to their respective types.

+ * + *

If the {@link Param.KeyValuePairs} annotation is specified, uses its subclass at {@link PairConverter} and allows passing either a map, a list + * like [key, value, key2, value2,...] or the same as the list inlined in the function's body, unless {@link Param.KeyValuePairs#allowMultiparam()} is + * set to {@code false}.

+ * + *

Maps provided by this converter are not linked to the initial map, and therefore will not reflect changes in either of them.

+ * + * @param The type of the map's keys + * @param The type of the map's values + */ +class MapConverter implements ValueConverter> +{ + protected final ValueConverter keyConverter; + protected final ValueConverter valueConverter; + + @Override + public String getTypeName() + { + return "map with " + keyConverter.getTypeName() + "s as the key and " + valueConverter.getTypeName() + "s as the value"; + } + + @Override + public Map convert(Value value) + { + Map result = new HashMap<>(); + if (value instanceof MapValue) + { + for (Entry entry : ((MapValue) value).getMap().entrySet()) + { + K key = keyConverter.convert(entry.getKey()); + V val = valueConverter.convert(entry.getValue()); + if (key == null || val == null) + return null; + result.put(key, val); + } + return result; + } + return null; + } + + private MapConverter(AnnotatedType keyType, AnnotatedType valueType) + { + keyConverter = ValueConverter.fromAnnotatedType(keyType); + valueConverter = ValueConverter.fromAnnotatedType(valueType); + } + + /** + *

Returns a new {@link MapConverter} to convert to the given {@link AnnotatedType}.

+ * + *

The returned {@link ValueConverter} will convert the objects inside the map (keys and values) to the generics specified in the + * {@link AnnotatedType}.

+ * + *

If the provided {@link AnnotatedType} is annotated with {@link Param.KeyValuePairs}, it will return an implementation of + * {@link MapConverter} that provides extra checks in order to allow inputs of either {@code {k->v,k2->v2,...}}, {@code [k,v,k2,v2,...]} or + * {@code fn(...,k,v,k2,v2,...)}

+ * + * @apiNote This method expects the {@link AnnotatedType} to already be of {@link Map} type, and, while it will technically accept a + * non-{@link Map} {@link AnnotatedType}, it will fail if it doesn't has at least two generic parameters with an + * {@link ArrayIndexOutOfBoundsException}. + * @param annotatedType The type to get generics information from + * @return A new {@link MapConverter} for the data specified in the {@link AnnotatedType} + */ + static MapConverter fromAnnotatedType(AnnotatedType annotatedType) + { + AnnotatedType[] annotatedGenerics = ((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments(); + return annotatedType.isAnnotationPresent(Param.KeyValuePairs.class) + ? new PairConverter<>(annotatedGenerics[0], annotatedGenerics[1], annotatedType.getAnnotation(Param.KeyValuePairs.class)) + : new MapConverter<>(annotatedGenerics[0], annotatedGenerics[1]); + } + + private static final class PairConverter extends MapConverter + { + private final boolean acceptMultiParam; + + private PairConverter(AnnotatedType keyType, AnnotatedType valueType, Param.KeyValuePairs config) + { + super(keyType, valueType); + acceptMultiParam = config.allowMultiparam(); + } + + @Override + public boolean consumesVariableArgs() + { + return acceptMultiParam; + } + + @Override + public Map convert(Value value) { + return value instanceof MapValue ? super.convert(value) + : value instanceof ListValue ? convertList(((ListValue)value).getItems()) + : null; // Multiparam mode can only be used in evalAndConvert + } + + + private Map convertList(List valueList) + { + if (valueList.size() % 2 == 1) + return null; + Map map = new HashMap<>(); + Iterator val = valueList.iterator(); + while (val.hasNext()) + { + K key = keyConverter.convert(val.next()); + V value = valueConverter.convert(val.next()); + if (key == null || value == null) + return null; + map.put(key, value); + } + return map; + } + + @Override + public Map checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + if (!valueIterator.hasNext()) + return null; + Value val = valueIterator.next(); + if (!acceptMultiParam || val instanceof MapValue || (val instanceof ListValue && !(keyConverter instanceof ListConverter))) + return convert(val); // @KeyValuePairs Map, Boolean> will not support list consumption + + Map map = new HashMap<>(); + K key = keyConverter.convert(val); //First pair is manual since we got it to check for a different conversion mode + V value = valueConverter.checkAndConvert(valueIterator, context, theLazyT); + if (key == null || value == null) + return null; + map.put(key, value); + while (valueIterator.hasNext()) + { + key = keyConverter.checkAndConvert(valueIterator, context, theLazyT); + value = valueConverter.checkAndConvert(valueIterator, context, theLazyT); + if (key == null || value == null) + return null; + map.put(key, value); + } + return map; + } + + @Override + public String getTypeName() + { + return "either a map of key-value pairs" + (acceptMultiParam ? "," : " or") + " a list in the form of [key, value, key2, value2,...]" + + (acceptMultiParam ? " or those key-value pairs in the function" : "") + " (keys being " + keyConverter.getTypeName() + + "s and values being " + valueConverter.getTypeName() + "s)"; + } + } +} diff --git a/src/main/java/carpet/script/annotation/OptionalConverter.java b/src/main/java/carpet/script/annotation/OptionalConverter.java new file mode 100644 index 00000000..e610e9c3 --- /dev/null +++ b/src/main/java/carpet/script/annotation/OptionalConverter.java @@ -0,0 +1,107 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Optional; + +import carpet.script.Context; +import carpet.script.value.Value; + +/** + *

{@link ValueConverter} that accepts a parameter to not be present on function call.

+ * + *

Note that it will not work properly if it's not at the end of the function, since values are consumed in an ordered way, therefore it will throw + * if the value is not present since it will try to evaluate the next parameter into {@code } and horribly fail, or it will draw values from the + * {@link ValueConverter}s after it, horribly failing too..

+ * + *

Why {@link Optional}?

+ * + *

It allows passing three states:

+ *
    + *
  • A wrapped object, if the value was present and correct
  • + *
  • An incorrect value ({@code null}), if the value was present but incorrect, as per the contract of {@link ValueConverter} and the way + * {@link AnnotationParser} will consider {@link null} values
  • + *
  • An empty {@link Optional}, if the value was not present (or {@code null})
  • + *
+ * + * @param The type of the internal {@link ValueConverter}, basically the generic type of the {@link Optional} + */ +final class OptionalConverter implements ValueConverter> +{ + private final ValueConverter typeConverter; + + @Override + public String getTypeName() + { + return "optional " + typeConverter.getTypeName(); + } + + private OptionalConverter(AnnotatedType type) + { + typeConverter = ValueConverter.fromAnnotatedType(type); + } + + /** + * {@inheritDoc} + * + * @implNote Unlike most other converters, {@link OptionalConverter} will not call this method from + * {@link #checkAndConvert(Iterator, Context, Context.Type)} and is only used as a fallback in types that don't support it. + */ + @Override + public Optional convert(Value value) + { + if (value.isNull()) + return Optional.empty(); + R converted = typeConverter.convert(value); + if (converted == null) + return null; + return Optional.of(converted); + } + + @Override + public Optional checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + if (!valueIterator.hasNext() || valueIterator.next().isNull()) + return Optional.empty(); + ((ListIterator) valueIterator).previous(); + R converted = typeConverter.checkAndConvert(valueIterator, context, theLazyT); + if (converted == null) + return null; + return Optional.of(converted); + } + + @Override + public boolean consumesVariableArgs() + { + return true; + } + + @Override + public int valueConsumption() + { + return 0; // Optional parameters therefore require a minimum of 0 + } + + /** + *

Returns a new {@link OptionalConverter} to convert to the type in the given {@link AnnotatedType} if there is at least one element left in + * the iterator, with any parameters, annotations or anything that was present in there.

+ * + *

The given {@link ValueConverter} will be called after ensuring that there is at least one element left in the iterator to the type specified + * in the generics of this {@link AnnotatedType}, or return an empty {@link Optional} if there is nothing left or the first value meets + * {@link Value#isNull()}. + * + * @apiNote This method expects the {@link AnnotatedType} to already be of {@link Optional} type, and, while it will technically accept a + * non-{@link Optional} {@link AnnotatedType}, it will fail with an {@link ArrayIndexOutOfBoundsException} if it doesn't has at least one + * generic parameter. + * @param annotatedType The type to get generics information from + * @return A new {@link OptionalConverter} for the data specified in the {@link AnnotatedType} + */ + static OptionalConverter fromAnnotatedType(AnnotatedType annotatedType) + { + AnnotatedParameterizedType paramType = (AnnotatedParameterizedType) annotatedType; + AnnotatedType wrappedType = paramType.getAnnotatedActualTypeArguments()[0]; + return new OptionalConverter<>(wrappedType); + } +} diff --git a/src/main/java/carpet/script/annotation/OutputConverter.java b/src/main/java/carpet/script/annotation/OutputConverter.java new file mode 100644 index 00000000..22bd7954 --- /dev/null +++ b/src/main/java/carpet/script/annotation/OutputConverter.java @@ -0,0 +1,129 @@ +package carpet.script.annotation; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import org.apache.commons.lang3.ClassUtils; + +import carpet.script.LazyValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import net.minecraft.entity.Entity; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.Tag; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.dynamic.GlobalPos; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; + +/** + *

A converter from a given {@link Object} of type T into a {@link LazyValue}, used in order to convert the outputs of methods into usable Scarpet + * values.

+ * + * @see #register(Class, Function) + * @see #registerToValue(Class, Function) + * @param The type to convert from into a {@link LazyValue} + */ +public final class OutputConverter +{ + private static final Map, OutputConverter> byResult = new HashMap<>(); + private static final OutputConverter VALUE = new OutputConverter<>(v -> (c, t) -> v); + static + { + register(LazyValue.class, Function.identity()); // Primitives are handled. Things are boxed in the process anyway, therefore + register(Boolean.class, v -> (v ? LazyValue.TRUE : LazyValue.FALSE)); // would recommend boxed outputs, so you can use null + register(Void.TYPE, v -> LazyValue.NULL); + registerToValue(Integer.class, NumericValue::of); + registerToValue(Double.class, NumericValue::of); + registerToValue(Float.class, NumericValue::of); + registerToValue(Long.class, NumericValue::of); + registerToValue(String.class, StringValue::new); + registerToValue(Entity.class, EntityValue::new); + registerToValue(Text.class, FormattedTextValue::new); + registerToValue(Tag.class, NBTSerializableValue::new); + registerToValue(BlockPos.class, ValueConversions::of); + registerToValue(Vec3d.class, ValueConversions::of); + registerToValue(ItemStack.class, ValueConversions::of); + registerToValue(Identifier.class, ValueConversions::of); + registerToValue(GlobalPos.class, ValueConversions::of); + } + + private final Function converter; + + private OutputConverter(Function converter) + { + this.converter = converter; + } + + /** + *

Returns the {@link OutputConverter} for the specified returnType.

+ * + * @param The type of the {@link OutputConverter} you are looking for + * @param returnType The class that the returned {@link OutputConverter} converts from + * @return The {@link OutputConverter} for the specified returnType + */ + @SuppressWarnings("unchecked") // OutputConverters are stored with their class, for sure since the map is private (&& class has same generic as + // converter) + public static OutputConverter get(Class returnType) + { + if (Value.class.isAssignableFrom(returnType)) + return (OutputConverter) VALUE; + returnType = (Class) ClassUtils.primitiveToWrapper(returnType); // wrapper holds same generic as primitive: wrapped + return (OutputConverter) Objects.requireNonNull(byResult.get(returnType), + "Unregistered output type: " + returnType + ". Register in OutputConverter"); + } + + /** + *

Converts the given input object into a {@link LazyValue}, to be used in return values of Scarpet functions

+ * + *

Returns {@link LazyValue#NULL} if passed a {@code null} input

+ * + * @param input The value to convert + * @return The converted value + */ + public LazyValue convert(T input) + { + return input == null ? LazyValue.NULL : converter.apply(input); + } + + /** + *

Registers a new type to be able to be used as the return value of methods, converting from inputType to a {@link LazyValue}.

+ * + * @see #registerToValue(Class, Function) + * @param The type of the input type + * @param inputType The class of T + * @param converter The function that converts the an instance of T to a {@link LazyValue} + */ + public static void register(Class inputType, Function converter) + { + OutputConverter instance = new OutputConverter(converter); + if (byResult.containsKey(inputType)) + throw new IllegalArgumentException(inputType + " already has a registered OutputConverter"); + byResult.put(inputType, instance); + } + + /** + *

Registers a new type to be able to be used as the return value of methods, converting from inputType to a {@link LazyValue}, with the + * converter returning a {@link Value} type.

+ * + * @see #register(Class, Function) + * @param The type of the input type + * @param inputType The class of T + * @param converter The function that converts an instance of T to a {@link Value} + */ + public static void registerToValue(Class inputType, Function converter) + { + OutputConverter instance = new OutputConverter<>(converter.andThen(v -> (c, t) -> v)); + if (byResult.containsKey(inputType)) + throw new IllegalArgumentException(inputType + " already has a registered OutputConverter"); + byResult.put(inputType, instance); + } +} diff --git a/src/main/java/carpet/script/annotation/Param.java b/src/main/java/carpet/script/annotation/Param.java new file mode 100644 index 00000000..4718dbc8 --- /dev/null +++ b/src/main/java/carpet/script/annotation/Param.java @@ -0,0 +1,323 @@ +package carpet.script.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.apache.commons.lang3.tuple.Pair; + +import carpet.CarpetServer; +import carpet.script.Context; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + *

Class that holds annotations for Scarpet parameters.

+ * + * @see Param.Strict + * @see Param.AllowSingleton + * @see Param.Custom + * @see Locator.Block + * @see Locator.Vec3d + * @see Locator.Function + * + */ +public interface Param +{ + /** + *

Determines that this parameter accepts being passing a value directly instead of a list of those values.

+ * + *

Can only be used in {@link List} parameters.

+ * + *

The function method will receive a singleton of the item in question if there's a single value.

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + public @interface AllowSingleton + { + + } + + /** + *

Determines that this (and optionally the following parameters) accept either a map of the specified key-value pairs, a list type [key, + * value, key2, value2,...] or the same as the list but directly in the parameters (can be disabled in {@link #allowMultiparam()}).

+ * + *

Can only be used in {@link Map} types, and {@link #allowMultiparam()} requires it to not be in a type parameter (since lists and maps + * contains groups of single items)

+ * + *

IMPORTANT: Using this annotation with {@link #allowMultiparam()} will make this element consume each and every remaining value in the + * function call, therefore it will cause any other parameters (that are not varargs) to throw as if they were not present, unless they are + * optional (defined by using Java's {@link Optional} type). They could only be accessed if the parameter at this location is specifically a list + * or map.
Having it as {@code true} will also cause the function to be considered of variable arguments even if it doesn't have varargs.

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + public @interface KeyValuePairs + { + /** + *

Whether or not this accepts the key-value pairs directly in the function call as myFunction(..., key, value, key2, value2)

+ * + *

Having this set to {@code true} (as it is by default) has the side effects of effectively converting the method in a variable parameter + * count method, and consuming everything remaining in the parameter list unless it finds as first parameter a map or list to generate the map + * from, causing any following parameters (except varargs) to throw as if they were not present, unless they are optional.

+ */ + boolean allowMultiparam() default true; + } + + /** + *

Defines that the parameter's converter has to be retrieved from the custom converter storage, in order to allow extensions to register + * complex {@link ValueConverter}s. You can register such converters in {@link Params#registerCustomConverterFactory(BiFunction)}, although + * if you only need a simple {@link Value}->something converter you should be looking at {@link SimpleTypeConverter} instead

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + public @interface Custom + { + + } + + /** + *

Defines that a parameter of type {@link String}, {@link Text}, {@link ServerPlayerEntity}, {@link Boolean} or other registered strict type + * must be of its corresponding {@link Value} in order to be accepted (respectively {@link StringValue}, {@link FormattedTextValue}, + * {@link EntityValue} or {@link BooleanValue}).

+ * + *

If this annotation is not specified, Carpet will accept any other {@link Value} and call respectively {@link Value#getString()}, + * {@code new LiteralText(Value#getString())}, {@link EntityValue#getPlayerByValue(MinecraftServer, Value)} or {@link Value#getBoolean()}.

+ * + *

You can define "shallow strictness" ({@link #shallow()}) if you want to allow passing both a {@link StringValue} or a + * {@link FormattedTextValue} to a {@link Text} parameter or a {@link NumericValue} to a {@link BooleanValue}, but not any {@link Value}.

+ * + */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + public @interface Strict + { + /** + *

Defines whether this parameter can accept types with "shallow strictness", that is, in order to get a {@link Text}, accepting either a + * {@link StringValue} or a {@link FormattedTextValue} as the parameter, or in order to get a {@link Boolean}, accepting either a + * {@link NumericValue} or a {@link BooleanValue}.

+ * + *

Without shallow mode, it would only accept from specifically a {@link FormattedTextValue} or {@link BooleanValue} respectively. + * + *

Using this in an unsupported type will throw {@link IllegalArgumentException}, just as if you used the annotation in an unsupported + * type.

+ * + *

This is {@code false} by default.

+ */ + boolean shallow() default false; + } + + /** + *

Class that holds the actual converters and converter getting logic for those annotated types and things.

+ * + *

It also holds the registry for strict {@link ValueConverter}s.

+ * + * @see #registerStrictConverter(Class, boolean, ValueConverter) + * + */ + public static final class Params + { + /** + *

A {@link ValueConverter} that outputs the given {@link LazyValue} when running {@link #checkAndConvert(Iterator, Context, Context.Type)}, and + * throws {@link UnsupportedOperationException} when trying to convert a {@link Value} directly.

+ * + *

Public in order to allow custom {@link ValueConverter} to check whether values should be evaluated while testing conditions.

+ */ + /*public static final ValueConverter LAZY_VALUE_IDENTITY = new ValueConverter() + { // No longer possible + + @Override + public LazyValue convert(Value val) + { + throw new UnsupportedOperationException( + "Called convert() with a Value in LazyValue identity, where only evalAndConvert is supported"); + } + + @Override + public LazyValue checkAndConvert(Iterator lazyValueIterator, Context c, Context.Type theLazyT) + { + return lazyValueIterator.hasNext() ? lazyValueIterator.next() : null; + } + + @Override + public String getTypeName() + { + return "something"; + } + };*/ + + /** + *

A {@link ValueConverter} that outputs the {@link Context} in which the function has been called when running + * {@link #checkAndConvert(Iterator, Context, Context.Type)}, and throws {@link UnsupportedOperationException} when trying to convert a {@link Value} + * directly.

+ */ + static final ValueConverter CONTEXT_PROVIDER = new ValueConverter() + { + @Override public String getTypeName() { return null; } + + @Override + public Context convert(Value value) + { + throw new UnsupportedOperationException("Called convert() with Value in Context Provider converter, where only checkAndConvert is supported"); + } + + @Override + public Context checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + return context; + } + + @Override + public int valueConsumption() + { + return 0; + } + }; + + /** + *

A {@link ValueConverter} that outputs the {@link Context.Type} which the function has been called when running + * {@link #checkAndConvert(Iterator, Context, Context.Type)}, or throws {@link UnsupportedOperationException} when trying to convert a {@link Value} + * directly.

+ */ + static final ValueConverter CONTEXT_TYPE_PROVIDER = new ValueConverter() + { + @Override public String getTypeName() { return null; } + + @Override + public Context.Type convert(Value value) + { + throw new UnsupportedOperationException("Called convert() with a Value in TheLazyT Provider, where only checkAndConvert is supported"); + } + + @Override + public Context.Type checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + return theLazyT; + } + + @Override + public int valueConsumption() + { + return 0; + } + }; + + /** + *

Strict converters

+ * + *

Stored as {@code , Converter>}

+ */ + private static Map, Boolean>, ValueConverter> strictParamsByClassAndShallowness = new HashMap<>(); + static + { // TODO Specify strictness in name? + registerStrictConverter(String.class, false, new SimpleTypeConverter<>(StringValue.class, StringValue::getString, "string")); + registerStrictConverter(Text.class, false, new SimpleTypeConverter<>(FormattedTextValue.class, FormattedTextValue::getText, "text")); + registerStrictConverter(Text.class, true, new SimpleTypeConverter<>(StringValue.class, FormattedTextValue::getTextByValue, "text")); + registerStrictConverter(ServerPlayerEntity.class, false, new SimpleTypeConverter<>(EntityValue.class, + v -> EntityValue.getPlayerByValue(CarpetServer.minecraft_server, v), "online player entity")); + registerStrictConverter(Boolean.class, false, new SimpleTypeConverter<>(BooleanValue.class, BooleanValue::getBoolean, "boolean")); + registerStrictConverter(Boolean.class, true, new SimpleTypeConverter<>(NumericValue.class, NumericValue::getBoolean, "boolean")); + } + + /** + * Ya' know, gets the {@link ValueConverter} given the {@link Strict} annotation. + * + * @param type The {@link AnnotatedType} to search the annotation data and class in + * @return The {@link ValueConverter} for the specified type and annotation data + * @throws IllegalArgumentException If the type doesn't accept the {@link Strict} annotation or if it has been used incorrectly (shallow in + * unsupported places) + */ + static ValueConverter getStrictConverter(AnnotatedType type) + { + boolean shallow = type.getAnnotation(Strict.class).shallow(); + Class clazz = (Class) type.getType(); + Pair, Boolean> key = Pair.of(clazz, shallow); + ValueConverter converter = strictParamsByClassAndShallowness.get(key); + if (converter != null) + return converter; + throw new IllegalArgumentException("Incorrect use of @Param.Strict annotation"); + } + + /** + *

Registers a new {@link Param.Strict} parameter converter with the specified shallowness.

+ * + *

Registered types should follow the general contract of the rest of {@link Param.Strict} parameter converters, that is, don't have a + * shallow-strict converter registered without having a fully-strict converter available. In order to register completely non-strict + * converters, those should be registered in their respective {@link ValueConverter} classes, usually in {@link SimpleTypeConverter}.

+ * + * @param The type class of the return type of the given converter and therefore the generic of itself + * @param type The class instance of the conversion result. + * @param shallow {@code true} if you are registering a shallow-strict parameter, {@code false} if a "fully" strict one + * @param converter The {@link ValueConverter} for the given type and shallowness. + */ + public static void registerStrictConverter(Class type, boolean shallow, ValueConverter converter) + { + Pair, Boolean> key = Pair.of(type, shallow); + if (strictParamsByClassAndShallowness.containsKey(key)) + throw new IllegalArgumentException(type + " already has a registered " + (shallow ? "" : "non-") + "shallow StrictConverter"); + strictParamsByClassAndShallowness.put(key, converter); + } + + private static List, ValueConverter>> customFactories = new ArrayList<>(); + + /** + *

Allows extensions to register COMPLEX {@link ValueConverter} factories in order to be used with the {@link Param.Custom} + * annotation.

If you only need to register a converter from a {@link Value} to a type, use + * {@link SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String)} instead. This is intended to be used when you + * need more granular control over the conversion, such as custom extra parameters via annotations, converters using multiple values, or even + * a variable number of values.

The annotation parser will loop through all registered custom converter factories when searching + * for the appropriate {@link ValueConverter} for a parameter annotated with the {@link Param.Custom} annotation.

Factories are + * expected to return {@code null} when the provided arguments don't match a {@link ValueConverter} they are able to create (or reuse).

+ *

You have {@link ValueCaster#get(Class)} and {@link ValueConverter#fromAnnotatedType(AnnotatedType)} available in case you need to get + * valid {@link ValueConverter}s for things such as nested types, intermediary conversions or whatever you really need them for.

+ * + * @param The type that the ValueConverter will convert to. Its class will also be passed to the factory + * @param factory A {@link BiFunction} that provides {@link ValueConverter}s given an {@link AnnotatedType} and the {@link Class} of its type, + * for convenience reasons. The factory must return {@code null} if the specific conditions required to return a valid + * converter are not met, therefore letting other registered factories try get theirs. Factories must also ensure that the + * returned {@link ValueConverter} converts to the given {@link Class}, and that the {@link ValueConverter} follows the + * contract of {@link ValueConverter}s, which can be found in its Javadoc. Factories should try to be specific in order to + * avoid possible collisions with other extensions. + */ + @SuppressWarnings("unchecked") // this makes no sense... But I guess its preferable to enforce typesafety in callers + public static void registerCustomConverterFactory(BiFunction, ValueConverter> factory) + { + customFactories.add((BiFunction, ValueConverter>) (Object) factory); + } + + @SuppressWarnings("unchecked") // Stored correctly + static ValueConverter getCustomConverter(AnnotatedType annoType, Class type) + { + ValueConverter result; + for (BiFunction, ValueConverter> factory : customFactories) + { + if ((result = (ValueConverter) factory.apply(annoType, type)) != null) + return result; + } + throw new IllegalArgumentException("No custom converter found for Param.Custom annotated param with type " + annoType.getType().getTypeName()); + } + } +} diff --git a/src/main/java/carpet/script/annotation/ScarpetFunction.java b/src/main/java/carpet/script/annotation/ScarpetFunction.java new file mode 100644 index 00000000..4083ada7 --- /dev/null +++ b/src/main/java/carpet/script/annotation/ScarpetFunction.java @@ -0,0 +1,85 @@ +package carpet.script.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Optional; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import carpet.script.Context; +import carpet.script.LazyValue; +import carpet.script.value.Value; + +/** + *

Defines a method that can be used as a function in the Scarpet language.

+ * + *

Methods annotated with this annotation are not required to accept and return the typical {@code Context context, Integer t, List lv}, but + * instead can specify whatever parameters they actually need that will be automatically converted from their respective {@link Value}s and passed to + * the method as the expected type. Functions will automatically fail if given parameters are not compatible with the specified ones, or if the number + * of provided arguments is either too large or too small.

+ * + *

Types to be used in those functions must be already registered in their respective {@link ValueConverter} implementations.
+ * In order to register a new type to convert to, you can do so in {@link SimpleTypeConverter#registerType(Class, Class, java.util.function.Function)}, + * and in order to register a new variant of {@link Value}, use {@link ValueCaster#register(Class, String)}.
+ * In order to convert the output of your method to a {@link LazyValue} you will also need to register its conversion in {@link OutputConverter}

+ * + *

In order for Carpet to find methods annotated with this annotation, you must add your function class(es) to Carpet by running + * {@link AnnotationParser#parseFunctionClass(Class)} ONCE. The provided {@link Class} must be concrete and provide the default constructor or an + * equivalent to it.

+ * + *

Methods annotated with this annotation must not be static and must not declare throwing any checked exceptions.

+ * + *

If one of the method's parameters is {@link Context}, Carpet will pass the actual {@link Context} of the expression to the + * method. If one of the method's parameters is {@link Context.Type}, Carpet will pass the Context Type the function was called inside. That + * is different from the Context Type provided in this annotation in that it's the one it was called with, while the one in the annotation + * is the one that will be used when evaluating the lazy values passed to the function.

+ * + * @see AnnotationParser + * @see AnnotationParser#parseFunctionClass(Class) + * @see Param.Strict + * @see Param.AllowSingleton + * @see Param.KeyValuePairs + * @see Param.Custom + * @see Locator.Block + * @see Locator.Vec3d + * @see Optional + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ScarpetFunction +{ + /** + *

If the function can accept a variable number of parameters, either by declaring its last parameter as a varargs parameter or by having one + * of their parameters use a converter that consumes a variable number of arguments, this must define the maximum number of parameters this + * function can take.

+ * + *

The parser will throw in case a function can accept a variable number of parameters but no maxParams value has been specified in its + * {@link ScarpetFunction} annotation.
+ * The value, however, will be ignored if the function has a fixed number of parameters.

+ * + *

Note that this maximum number of parameters refers to the limit of parameters that can be passed to a function from Scarpet, not the maximum + * number of parameters the method will receive in its varargs parameter. Therefore, if using, for example, a locator argument, you should + * consider that those can take either a single triple of values or 3 independent values, that would be counted in the maximum number of + * parameters.

+ * + *

Use -1 to specify an unlimited number of parameters.

+ * + * @return The maximum number of parameters this function can accept + */ + int maxParams() default -2; + + /** + *

Defines the Context Type that will be used when evaluating arguments to annotated methods.

+ * + *

Note that this is not the same as the output from a {@link Context.Type} parameter, since that returns the Context Type the method was + * called with, while this defines what Context Type will be used to evaluate the arguments.

+ * + *

Defaults to {@link Context.Type#NONE}, like any regular ContextFunctions

+ * + * @see Context + */ + Context.Type contextType() default Context.Type.NONE; +} diff --git a/src/main/java/carpet/script/annotation/SimpleTypeConverter.java b/src/main/java/carpet/script/annotation/SimpleTypeConverter.java new file mode 100644 index 00000000..db307ef5 --- /dev/null +++ b/src/main/java/carpet/script/annotation/SimpleTypeConverter.java @@ -0,0 +1,112 @@ +package carpet.script.annotation; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import carpet.CarpetServer; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import net.minecraft.entity.Entity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.world.World; + +/** + *

A simple {@link ValueConverter} implementation that converts from a specified subclass of {@link Value} into {@code } by using a given + * function.

+ * + *

{@link SimpleTypeConverter}s are reused whenever asked for one, since they don't have any complexity.

+ * + * @see #registerType(Class, Class, Function) + * + * @param The type of the required input {@link Value} + * @param The type that this converter converts to + */ +public final class SimpleTypeConverter implements ValueConverter +{ + private static final Map, SimpleTypeConverter> byResult = new HashMap<>(); + static + { + registerType(Value.class, ServerPlayerEntity.class, val -> EntityValue.getPlayerByValue(CarpetServer.minecraft_server, val), "online player"); + registerType(EntityValue.class, Entity.class, EntityValue::getEntity, "entity"); + registerType(Value.class, World.class, val -> ValueConversions.dimFromValue(val, CarpetServer.minecraft_server), "dimension"); + registerType(Value.class, Text.class, FormattedTextValue::getTextByValue, "text"); + registerType(Value.class, String.class, Value::getString, "string"); // Check out @Param.Strict for more specific types + + // Primitives are also query those classes + registerType(NumericValue.class, Long.class, NumericValue::getLong, "number"); + registerType(NumericValue.class, Double.class, NumericValue::getDouble, "number"); + registerType(NumericValue.class, Integer.class, NumericValue::getInt, "number"); + registerType(Value.class, Boolean.class, Value::getBoolean, "boolean"); // Check out @Param.Strict for more specific types + } + + private final Function converter; + private final Class valueClass; + private final String typeName; + + /** + *

The default constructor for {@link SimpleTypeConverter}.

+ * + *

This is public in order to provide an implementation to use when registering {@link ValueConverter}s for the {@link Param.Strict} annotation + * registry, and it's not intended way to register new {@link SimpleTypeConverter}

Use {@link #registerType(Class, Class, Function)} for + * that.

+ * + * @param inputType The required type for the input {@link Value} + * @param converter The function to convert an instance of inputType into R. + */ + public SimpleTypeConverter(Class inputType, Function converter, String typeName) + { + this.converter = converter; + this.valueClass = inputType; + this.typeName = typeName; + } + + @Override + public String getTypeName() + { + return typeName; + } + + /** + * Returns the {@link SimpleTypeConverter} for the specified outputType. + * + * @param The type of the {@link SimpleTypeConverter} you are looking for + * @param outputType The class that the returned {@link SimpleTypeConverter} converts to + * @return The {@link SimpleTypeConverter} for the specified outputType + */ + @SuppressWarnings("unchecked") // T always extends Value, R is always the same as map's key, since map is private. + static SimpleTypeConverter get(Class outputType) + { + return (SimpleTypeConverter) byResult.get(outputType); + } + + @Override + public R convert(Value value) + { + return valueClass.isInstance(value) ? converter.apply(valueClass.cast(value)) : null; + } + + /** + *

Registers a new conversion from a {@link Value} subclass to a Java type.

+ * + * @param The {@link Value} subtype required for this conversion, for automatic checked casting + * @param The type of the resulting object + * @param requiredInputType The {@link Class} of {@code } + * @param outputType The {@link Class} of {@code >} + * @param converter A function that converts from the given {@link Value} subtype to the given type. Should ideally return {@code null} + * when given {@link Value} cannot be converted to the {@code }, to follow the {@link ValueConverter} contract, but it + * can also throw an {@link InternalExpressionException} by itself if really necessary. + * @param typeName The name of the type, following the conventions of {@link ValueConverter#getTypeName()} + */ + public static void registerType(Class requiredInputType, Class outputType, + Function converter, String typeName) + { + SimpleTypeConverter type = new SimpleTypeConverter<>(requiredInputType, converter, typeName); + byResult.put(outputType, type); + } +} diff --git a/src/main/java/carpet/script/annotation/ValueCaster.java b/src/main/java/carpet/script/annotation/ValueCaster.java new file mode 100644 index 00000000..539995d8 --- /dev/null +++ b/src/main/java/carpet/script/annotation/ValueCaster.java @@ -0,0 +1,101 @@ +package carpet.script.annotation; + +import java.util.HashMap; +import java.util.Map; + +import carpet.script.value.AbstractListValue; +import carpet.script.value.BlockValue; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.ThreadValue; +import carpet.script.value.Value; + +/** + *

Simple {@link ValueConverter} implementation that casts a {@link Value} into one of its subclasses, either for use directly in parameters or + * converters, or as an already working middle step.

+ * + *

{@link ValueCaster}s are reused whenever asked for one, since they don't have any complexity.

+ * + * @see #register(Class, String) + * + * @param The {@link Value} subclass this {@link ValueCaster} casts to + */ +public final class ValueCaster implements ValueConverter // R always extends Value, not explicitly because of type checking +{ + private static final Map, ValueCaster> byResult = new HashMap<>(); + static + { + register(Value.class, "value"); + register(BlockValue.class, "block"); + register(EntityValue.class, "entity"); + register(FormattedTextValue.class, "formatted text"); + register(FunctionValue.class, "function"); + register(ListValue.class, "list"); + register(MapValue.class, "map"); + register(AbstractListValue.class, "list or similar"); // For LazyListValue basically? Not sure what should use this + register(NBTSerializableValue.class, "nbt object"); + register(NumericValue.class, "number"); + register(BooleanValue.class, "boolean"); + register(StringValue.class, "string"); + register(ThreadValue.class, "thread"); + } + + private final Class outputType; + private final String typeName; + + private ValueCaster(Class outputType, String typeName) + { + this.outputType = outputType; + this.typeName = typeName; + } + + @Override + public String getTypeName() + { + return typeName; + } + + /** + *

Returns the registered {@link ValueCaster} for the specified outputType.

+ * + * @param The type of the {@link ValueCaster} you are looking for + * @param outputType The class of the {@link Value} the returned {@link ValueCaster} casts to + * @return The {@link ValueCaster} for the specified outputType + */ + @SuppressWarnings("unchecked") // Casters are stored with their exact class, for sure since the map is private (&& class has same generic as + // caster) + public static ValueCaster get(Class outputType) + { + return (ValueCaster) byResult.get(outputType); + } + + @Override + public R convert(Value value) + { + if (!outputType.isInstance(value)) + return null; + return outputType.cast(value); + } + + /** + *

Registers a new {@link Value} to be able to use it in {@link SimpleTypeConverter}

+ * + * @param The {@link Value} subclass + * @param valueClass The class of T + * @param typeName A {@link String} representing the name of this type. It will be used in error messages when there is no higher type + * required + */ + public static void register(Class valueClass, String typeName) + { + ValueCaster caster = new ValueCaster(valueClass, typeName); + byResult.putIfAbsent(valueClass, caster); + } +} diff --git a/src/main/java/carpet/script/annotation/ValueConverter.java b/src/main/java/carpet/script/annotation/ValueConverter.java new file mode 100644 index 00000000..36d214bd --- /dev/null +++ b/src/main/java/carpet/script/annotation/ValueConverter.java @@ -0,0 +1,177 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.ParameterizedType; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.lang3.ClassUtils; +import org.jetbrains.annotations.Nullable; + +import carpet.script.Context; +import carpet.script.annotation.Param.Params; +import carpet.script.value.Value; + +/** + *

Classes implementing this interface are able to convert {@link Value} instances into {@code }, in order to easily use them in parameters for + * Scarpet functions created using the {@link ScarpetFunction} annotation.

+ * + * @param The result type that the passed {@link Value}s will be converted to + */ +public interface ValueConverter +{ + /** + *

Returns the the user-friendly name of the result this {@link ValueConverter} converts to, without {@code a} or {@code an}, and without + * capitalizing the first letter.

+ * + *

This method can return {@code null}, in which case users of this function should hide this {@link ValueConverter} from any aids or usages of + * the function, meaning that the {@link ValueConverter} is only providing some meta information that isn't directly provided through the Scarpet + * language.

+ * + *

Those aids calling this method may append an {@code s} to the return value of this method, in case the type is used in places where more + * than one may be present, such as lists or varargs.

+ * + * @apiNote This method is intended to only be called when an error has occurred and therefore there is a need to print a stacktrace with some + * helpful usage instructions. + */ + public String getTypeName(); + + /** + *

Converts the given {@link Value} to {@code }, which was defined when being registered.

+ * + *

Returns {@code null} if one of the conversions failed, either because the {@link Value} was incompatible in some position of the chain, or + * because the actual converting function returned {@code null} (which usually only occurs when the {@link Value} is incompatible/does not hold + * the appropriate information)

+ * + *

Functions using the converter can use {@link #getTypeName()} to get the name of the type this was trying to convert to, in case they are not + * trying to convert to anything else, where it would be recommended to tell the user the name of the final type instead.

+ * + * @param value The {@link Value} to convert + * @return The converted value, or {@code null} if the conversion failed in the process + * @apiNote

While most implementations of this method should and will return the type from this method, implementations that require + * parameters from {@link #checkAndConvert(Iterator, Context, Context.Type)} or that require multiple parameters may decide to throw + * {@link UnsupportedOperationException} in this method and override {@link #checkAndConvert(Iterator, Context, Context.Type)} instead. Those + * implementations, however, should not be available for map or list types, since those can only operate with {@link Value}.

+ *

Currently, the only implementations requiring that are {@link Params#CONTEXT_PROVIDER} and {@link Params#CONTEXT_TYPE_PROVIDER}

+ *

Implementations can also provide different implementations for this and {@link #checkAndConvert(Iterator, Context, Context.Type)}, in case + * they can support it in some situations that can't be used else, such as inside of lists or maps, although they should try to provide + * in {@link #checkAndConvert(Iterator, Context, Context.Type)} at least the same conversion as the one from this method.

+ *

Even with the above reasons, {@link ValueConverter} users should try to implement {@link #convert(Value)} whenever possible instead of + * {@link #checkAndConvert(Iterator, Context, Context.Type)}, since it allows its usage in generics of lists and maps.

+ */ + @Nullable + public R convert(Value value); + + /** + *

Returns whether this {@link ValueConverter} consumes a variable number of elements from the {@link Iterator} passed to it via + * {@link #checkAndConvert(Iterator, Context, Context.Type)}.

+ * + * @implNote The default implementation returns {@code false} + * @see #valueConsumption() + */ + default public boolean consumesVariableArgs() + { + return false; + } + + /** + *

Declares the number of {@link Value}s this converter consumes from the {@link Iterator} passed to it in + * {@link #checkAndConvert(Iterator, Context, Context.Type)}.

+ * + *

If this {@link ValueConverter} can accept a variable number of arguments (therefore the result of calling {@link #consumesVariableArgs()} + * must return {@code true}), it will return the minimum number of arguments it will consume.

+ * + * @implNote The default implementation returns {@code 1} + * + */ + default public int valueConsumption() + { + return 1; + } + + /** + *

Gets the proper {@link ValueConverter} for the given {@link AnnotatedType}, considering the type of {@code R[]} as {@code R}.

+ * + *

This function does not only consider the actual type (class) of the passed {@link AnnotatedType}, but also its annotations and generic + * parameters in order to get the most specific {@link ValueConverter}.

+ * + *

Some processing is delegated to the appropriate implementations of {@link ValueConverter} in order to get registered converters or generate + * specific ones for some functions.

+ * + * @param The type of the class the returned {@link ValueConverter} will convert to. It is declared from the type in the + * {@link AnnotatedType} directly inside the function. + * @param annoType The {@link AnnotatedType} to search a {@link ValueConverter}. + * @return A usable {@link ValueConverter} to convert from a {@link Value} to {@code } + */ + @SuppressWarnings("unchecked") + public static ValueConverter fromAnnotatedType(AnnotatedType annoType) + { + Class type = annoType.getType() instanceof ParameterizedType ? // We are defining R here. + (Class) ((ParameterizedType) annoType.getType()).getRawType() : + (Class) annoType.getType(); + // I (altrisi) won't implement generics in varargs. Those are just PAINFUL. They have like 3-4 nested types and don't have the generics + // and annotations in the same place, plus they have a different "conversion hierarchy" than the rest, making everything require + // special methods to get the class from type, generics from type and annotations from type. Not worth the effort for me. + // Example: AnnotatedGenericTypeArray (or similar) being (@Paran.KeyValuePairs Map... name) + // Those will just fail with a ClassCastException. + if (type.isArray()) + type = (Class) type.getComponentType(); // Varargs + type = (Class) ClassUtils.primitiveToWrapper(type); // It will be boxed anyway, this saves unboxing-boxing + if (type == List.class) + return (ValueConverter) ListConverter.fromAnnotatedType(annoType); // Already checked that type is List + if (type == Map.class) + return (ValueConverter) MapConverter.fromAnnotatedType(annoType); // Already checked that type is Map + if (type == Optional.class) + return (ValueConverter) OptionalConverter.fromAnnotatedType(annoType); + if (annoType.getDeclaredAnnotations().length != 0) + { + if (annoType.isAnnotationPresent(Param.Custom.class)) + return Param.Params.getCustomConverter(annoType, type); // Throws if incorrect usage + if (annoType.isAnnotationPresent(Param.Strict.class)) + return (ValueConverter) Params.getStrictConverter(annoType); // Throws if incorrect usage + if (annoType.getAnnotations()[0].annotationType().getEnclosingClass() == Locator.class) + return Locator.Locators.fromAnnotatedType(annoType, type); + } + + // Class only checks + if (type.isAssignableFrom(Value.class)) + return Objects.requireNonNull(ValueCaster.get(type), "Value subclass " + type + " is not registered. Register it in ValueCaster to use it"); + // if (type == LazyValue.class) // No longer supported + // return (ValueConverter) Params.LAZY_VALUE_IDENTITY; + if (type == Context.class) + return (ValueConverter) Params.CONTEXT_PROVIDER; + if (type == Context.Type.class) + return (ValueConverter) Params.CONTEXT_TYPE_PROVIDER; + return Objects.requireNonNull(SimpleTypeConverter.get(type), "Type " + type + " is not registered. Register it in SimpleTypeConverter to use it"); + } + + /** + *

Checks for the presence of the next {@link Value} in the given {@link Iterator}, evaluates it with the given {@link Context} and then + * converts it to this {@link ValueConverter}'s output type.

+ * + *

This should be the preferred way to call the converter, since it allows multi-param converters and allows meta converters (such as the {@link Context} provider)

+ * + * @implSpec Implementations of this method are not required to move the {@link Iterator} to the next position, such as in the case of meta + * providers like {@link Context}.

Implementations can also use more than a single parameter when being called with this function, + * but in such case they must implement {@link #valueConsumption()} to return how many parameters do they consume at minimum, and, if + * they may consume variable arguments, implement {@link #consumesVariableArgs()}

This method holds the same nullability + * constraints as {@link #convert(Value)}

+ * @param valueIterator An {@link Iterator} holding the {@link Value} to convert in next position + * @param context The {@link Context} this function has been called with. This was used when this passed lazy values instead in order to + * evaluate, now it's used to get the context. It is now ignored by the default implementation + * @param theLazyT The {@code t} that the original function was called with. It is ignored by the default implementation. + * @return The next {@link Value} (s) converted to the type {@code } of this {@link ValueConverter} + * @implNote This method's default implementation runs the {@link #convert(Value)} function in the next {@link Value} ignoring {@link Context} and + * {@code theLazyT}. + */ + default public R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + if (!valueIterator.hasNext()) + return null; + return convert(valueIterator.next()); + } +}