Annotated Scarpet (#772)
* Progress by now. * Output conversions plus a few extra random things * More progress, mostly list converting and a way to get converters * Apply ValueConverters, refactor valueconverter getter to accept types with annotations and generics, made listconverter pass those params and annotations recursively. And probably more things. * Some extra docs, the 100% most important thing * Added MapConverter, strict Strings and Texts, some docs additions and commented out heuristics * Rework StrictParam to use SimpleTypeConverters and be saved in a map * Minor changes and docs & added non-fully-functional context "converter" * Added support for variable value count converters, multi-value eaters & 0 value eaters (Context). * Docs update, enable Context converter, fix minor TODOs * Move (most) parameter annotations into a single class, remove deprecated ValueCaster#cast, update some docs. * Small refactor of Param.Strict registering, added boolean to it * Added PairConverter, moved Context and LV ValueConverters to Params * Document public methods of AnnotationParser * Add a lot more docs, make it ready for the first preview PR * Move ValueConverter population outside of makeFunction, check lv size for > max arguments, minor doc updates and workaround for Mercury#28 until released * Added optional parameters (declared using Java's Optional), made evalAndConvert check for the presence of the next iteration, updated test class * Generalize Value OutputConverter * ListConverter and MapConverter now follow ValueConverter contract of incorrect value handling, returning a complete null for an incorrect value anywhere. Also small fix to OptionalConverter and docs. Getting ready for checking null in AnnotationParser! * Change reflection's #invoke to a MethodHandle to boost speed by a lot Commented * Dedupe code, update and remove outdated todos, add 2 new ValueCasters * Merge lambdas in display_title2 test function to be more accurate * Check nulls somehow, I guess * Move base function logic into a class to stop passing hundreds of parameters into static functions and therefore simplify it * Reduce visibility and fix a bug with PairConverter * Hide Context provider in usage report * Add support for primitive varargs * Added TheLazyT converter * Drop #getPrefixedTypeName and use 'something' as name of values * Use value again because of varargs, document that * Remove a completed TODO * Remove old disabled heuristic value type name getter * Fix TheLazyT * Preparing locators before touching current Carpet infrastructure * Add notes about non-implementedness of remaining locators * Added APIs to register custom ValueConverters * Mention new APIs in javadocs * Adapt to new Carpet changes and deprecated non-implemented annotations * Update example function for display_title() * Better implementation for player_list example * Fix example error message * FunctionArgument no longer has generics * Modernize Text converters * OutputConverter is final, plus other small changes * Few non-behavioural changes * Rename LazyFunction to ScarpetFunction * Drop LazyValue in parameters, not in return Updating docs was a nightmare * CARPET CHANGE - Make Fluff.AbstractLazyFunction.unpackArgs call unpackLazy (was duped) * Make errors more like current Scarpet errors * Allow customizing ContextType when evaluating arguments * Add a few missing docs, comment out outdated ValueCaster doc It's been a while, wow * Fix for new Carpet signature :irritatered: Please don't break API methods! Extensions may be using them, and they will just crash the game or behave in unexpected ways else! * Format all code to Carpet's. Powered by Eclipse Reviewed to be sure it's ok. No functional changes, except from a small correction * Always use full name for Context.Type * Add parsing for all current Scarpet api and language classes And remove test functions * Allow void return type * Fix no param methods Whoops! * Remove outdated comment and fix small javadoc linting problem * Drop Param.TheLazyT in favour of just Context.Type type And small performance improvement to varargs varargs since fastutil's toArray is crap * Quote functions in error messages * Make SimpleTypeConverter independent of ValueCaster 'Real world' usage demonstrated it can be annoying else, mostly for wrapper only values where you just want the wrapped object
This commit is contained in:
1 parent
7fd49e0f04
commit
cc7a9c01be
16 files changed
+1976
-9
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -125,15 +125,7 @@ public abstract class Fluff
|
||||
|
||||
public List<Value> unpackArgs(List<LazyValue> lzargs, Context c, Context.Type contextType)
|
||||
{
|
||||
List<Value> 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<Value> args = unpackLazy(lzargs, c, contextType);
|
||||
if (!numParamsVaries() && getNumParams() != args.size())
|
||||
throw new InternalExpressionException("Function " + getName() + " expected " + getNumParams() + " parameters, got " + args.size());
|
||||
return args;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>To begin, use the {@link #parseFunctionClass(Class)} method in a class with methods annotated with the {@link ScarpetFunction} annotation.</p>
|
||||
*
|
||||
* @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<ParsedFunction> functionList = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* <p>Parses a given {@link Class} and registers its annotated methods, the ones with the {@link ScarpetFunction} annotation,
|
||||
* to be used in the Scarpet language.</p>
|
||||
*
|
||||
* <p><b>Only call this method once per class per lifetime of the JVM!</b> (for example, at {@link CarpetExtension#onGameStarted()})</p>
|
||||
*
|
||||
* <p>There is a set of requirements for the class and its methods:</p>
|
||||
* <ul>
|
||||
* <li>Class must be concrete. That is, no interfaces or abstract classes should be passed</li>
|
||||
* <li>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.</li>
|
||||
* <li>Annotated methods must not be {@code static}. See claim above</li>
|
||||
* <li>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.</li>
|
||||
* <li>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()}.</li>
|
||||
* <li>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}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see ScarpetFunction
|
||||
* @param <T> The generic type of the class to parse.
|
||||
* @param clazz The class to parse
|
||||
*/
|
||||
public static <T> void parseFunctionClass(Class<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Adds all parsed functions to the given {@link Expression}.</p>
|
||||
* <p>This is handled automatically by Carpet</p>
|
||||
*
|
||||
* @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<Context, Context.Type, List<LazyValue>, LazyValue>
|
||||
{
|
||||
private final String name;
|
||||
private final boolean isMethodVarArgs;
|
||||
private final int methodParamCount;
|
||||
private final List<ValueConverter<?>> valueConverters;
|
||||
private final Class<?> varArgsType;
|
||||
private final boolean primitiveVarArgs;
|
||||
private final ValueConverter<?> varArgsConverter;
|
||||
private final OutputConverter<Object> 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<Object> converter = OutputConverter.get((Class<Object>) 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<LazyValue> lazyValues)
|
||||
{
|
||||
List<Value> 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<Value> lv, final Context context, Context.Type theLazyT)
|
||||
{
|
||||
Object[] params = new Object[methodParamCount];
|
||||
ListIterator<Value> 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<Object> 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() {}
|
||||
}
|
||||
@@ -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<ServerPlayerEntity> targets, String actionString, Optional<Text> 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<ServerPlayerEntity, BaseText> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Converts a given {@link ListValue} into a {@link List} of values converted to {@code <T>}.</p>
|
||||
*
|
||||
* <p>If the {@link Param.AllowSingleton} annotation is specified, allows creating a singleton from
|
||||
* a loose element compatible with the type conversion.</p>
|
||||
*
|
||||
* <p>Lists provided by this converter are <b>not</b> linked to the initial list, and therefore will not
|
||||
* reflect changes in either of them</p>
|
||||
*
|
||||
* @param <T> The type of the element that will be inside the list
|
||||
*/
|
||||
final class ListConverter<T> implements ValueConverter<List<T>>
|
||||
{
|
||||
private final ValueConverter<T> itemConverter;
|
||||
private final boolean allowSingletonCreation;
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return (allowSingletonCreation ? itemConverter.getTypeName() + " or " : "") + "list of " + itemConverter.getTypeName() + "s";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> convert(Value value)
|
||||
{
|
||||
return value instanceof ListValue ? convertListValue((ListValue) value) : allowSingletonCreation ? convertSingleton(value) : null;
|
||||
}
|
||||
|
||||
private List<T> convertListValue(ListValue values)
|
||||
{
|
||||
List<T> 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<T> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns a new {@link ListConverter} to convert to the given {@link AnnotatedType}.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class that holds annotation for {@link Argument} locators to be used in Scarpet functions.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
public interface Locator
|
||||
{
|
||||
/**
|
||||
* <p>Represents that the annotated argument must be gotten by passing the arguments in there into a {@link BlockArgument} locator.</p>
|
||||
*
|
||||
* <p>Must be used in either {@link BlockValue} or {@link BlockArgument} parameters</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
@Deprecated // Not implemented, although implementation available
|
||||
public @interface Block
|
||||
{
|
||||
/**
|
||||
* <p>Whether or not should the locator accept a single {@link String} as the parameter and let parsing to {@link BlockValue}.</p>
|
||||
*/
|
||||
boolean acceptString() default false;
|
||||
|
||||
/**
|
||||
* <p>Whether or not should the {@link BlockValue} argument be optional.</p> <p>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.</p>
|
||||
*/
|
||||
boolean optional() default false;
|
||||
|
||||
/**
|
||||
* <p>Whether or not should the {@link BlockArgument} locator accept any string as the argument.</p> <p>Requires the annotation to be present
|
||||
* in a {@link BlockArgument} type, since it may just return a {@link String}</p>
|
||||
*/
|
||||
boolean anyString() default false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Represents that the annotated argument must be gotten by passing the arguments in there into a {@link Vector3Argument} locator.</p>
|
||||
*
|
||||
* <p>Must be used in either a {@link Vector3Argument} or a {@link Vec3d} parameter, though the latest may not get all the data.</p>
|
||||
*
|
||||
* <p>Note: This locator has not been implemented yet (TODO)</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
@Deprecated // Not implemented
|
||||
public @interface Vec3d
|
||||
{
|
||||
/**
|
||||
* <p>Whether or not should the {@link Vector3Argument} locator accept an optional direction aside from the
|
||||
* {@link net.minecraft.util.math.Vec3d}</p> <p>This parameter can only be used in a {@link Vector3Argument} type, since else there is no way
|
||||
* to get the direction too.</p>
|
||||
*/
|
||||
boolean optionalDirection() default false;
|
||||
|
||||
/**
|
||||
* <p>Whether or not should the {@link Vector3Argument} locator accept an entity aside to get the {@link Vec3d} from and return that entity
|
||||
* too</p> <p>Note that you will only be able to get that entity if the annotation is present in a {@link Vector3Argument}</p>
|
||||
*/
|
||||
boolean optionalEntity() default false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Represents that the annotated argument must be gotten by passing the arguments in this annotation into a {@link FunctionArgument}
|
||||
* locator</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p><b>This will consume any remaining parameters passed to the function, therefore any other parameters after this will throw.</b></p>
|
||||
*
|
||||
* <p>Note: This locator has not been implemented yet (TODO)</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
@Deprecated // Not implemented
|
||||
public @interface Function
|
||||
{
|
||||
/**
|
||||
* <p>Whether this Locator should allow no function to be passed.</p> <p>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}</p>
|
||||
*/
|
||||
boolean allowNone() default false;
|
||||
|
||||
/**
|
||||
* <p>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}.</p>
|
||||
*/
|
||||
boolean checkArgs();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Class that holds locators and methods to get them</p>
|
||||
*
|
||||
* <p>Not part of the public API, just that interfaces must have all members public</p>
|
||||
*/
|
||||
static final class Locators
|
||||
{
|
||||
private Locators() {}
|
||||
|
||||
static <R> ValueConverter<R> fromAnnotatedType(AnnotatedType annoType, Class<R> type)
|
||||
{
|
||||
if (annoType.isAnnotationPresent(Block.class))
|
||||
return new BlockLocator<R>(annoType.getAnnotation(Block.class), type);
|
||||
if (annoType.isAnnotationPresent(Function.class))
|
||||
return new FunctionLocator<R>(annoType.getAnnotation(Function.class), type);
|
||||
if (annoType.isAnnotationPresent(Vec3d.class))
|
||||
return new Vec3dLocator<R>(annoType.getAnnotation(Vec3d.class), type);
|
||||
throw new IllegalStateException("Locator#fromAnnotatedType got called with an incompatible AnnotatedType");
|
||||
}
|
||||
|
||||
private static class BlockLocator<R> extends AbstractLocator<R>
|
||||
{
|
||||
private final boolean returnBlockValue;
|
||||
private final boolean acceptString;
|
||||
private final boolean anyString;
|
||||
private final boolean optional;
|
||||
|
||||
public BlockLocator(Block annotation, Class<R> 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<Value> 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<R> extends AbstractLocator<R>
|
||||
{
|
||||
private final boolean optionalDirection;
|
||||
private final boolean optionalEntity;
|
||||
private final boolean returnVec3d;
|
||||
|
||||
public Vec3dLocator(Vec3d annotation, Class<R> 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<Value> 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<R> extends AbstractLocator<R>
|
||||
{
|
||||
private final boolean returnFunctionValue;
|
||||
private final boolean allowNone;
|
||||
private final boolean checkArgs;
|
||||
|
||||
FunctionLocator(Function annotation, Class<R> 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<Value> 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<R> implements ValueConverter<R>, 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<Value> valueIterator, Context context, Context.Type theLazyT);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Converts a {@link MapValue} to a {@link Map}, converting all of its contents to their respective types.</p>
|
||||
*
|
||||
* <p>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}.</p>
|
||||
*
|
||||
* <p>Maps provided by this converter are <b>not</b> linked to the initial map, and therefore will not reflect changes in either of them.</p>
|
||||
*
|
||||
* @param <K> The type of the map's keys
|
||||
* @param <V> The type of the map's values
|
||||
*/
|
||||
class MapConverter<K, V> implements ValueConverter<Map<K, V>>
|
||||
{
|
||||
protected final ValueConverter<K> keyConverter;
|
||||
protected final ValueConverter<V> valueConverter;
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "map with " + keyConverter.getTypeName() + "s as the key and " + valueConverter.getTypeName() + "s as the value";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<K, V> convert(Value value)
|
||||
{
|
||||
Map<K, V> result = new HashMap<>();
|
||||
if (value instanceof MapValue)
|
||||
{
|
||||
for (Entry<Value, Value> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns a new {@link MapConverter} to convert to the given {@link AnnotatedType}.</p>
|
||||
*
|
||||
* <p>The returned {@link ValueConverter} will convert the objects inside the map (keys and values) to the generics specified in the
|
||||
* {@link AnnotatedType}.</p>
|
||||
*
|
||||
* <p>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,...)}</p>
|
||||
*
|
||||
* @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<K, V> extends MapConverter<K, V>
|
||||
{
|
||||
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<K, V> 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<K, V> convertList(List<Value> valueList)
|
||||
{
|
||||
if (valueList.size() % 2 == 1)
|
||||
return null;
|
||||
Map<K, V> map = new HashMap<>();
|
||||
Iterator<Value> 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<K, V> checkAndConvert(Iterator<Value> 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<List<Something>, Boolean> will not support list consumption
|
||||
|
||||
Map<K, V> 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)";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>{@link ValueConverter} that accepts a parameter to not be present on function call.</p>
|
||||
*
|
||||
* <p>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 <R>} and horribly fail, or it will draw values from the
|
||||
* {@link ValueConverter}s after it, horribly failing too..</p>
|
||||
*
|
||||
* <p>Why {@link Optional}?</p>
|
||||
*
|
||||
* <p>It allows passing three states:</p>
|
||||
* <ul>
|
||||
* <li>A wrapped object, if the value was present and correct</li>
|
||||
* <li>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</li>
|
||||
* <li>An empty {@link Optional}, if the value was not present (or {@code null})</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param <R> The type of the internal {@link ValueConverter}, basically the generic type of the {@link Optional}
|
||||
*/
|
||||
final class OptionalConverter<R> implements ValueConverter<Optional<R>>
|
||||
{
|
||||
private final ValueConverter<R> 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<R> 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<R> checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
if (!valueIterator.hasNext() || valueIterator.next().isNull())
|
||||
return Optional.empty();
|
||||
((ListIterator<Value>) 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
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @see #register(Class, Function)
|
||||
* @see #registerToValue(Class, Function)
|
||||
* @param <T> The type to convert from into a {@link LazyValue}
|
||||
*/
|
||||
public final class OutputConverter<T>
|
||||
{
|
||||
private static final Map<Class<?>, OutputConverter<?>> byResult = new HashMap<>();
|
||||
private static final OutputConverter<Value> 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<T, LazyValue> converter;
|
||||
|
||||
private OutputConverter(Function<T, LazyValue> converter)
|
||||
{
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the {@link OutputConverter} for the specified returnType.</p>
|
||||
*
|
||||
* @param <T> 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 <T> OutputConverter<T> get(Class<T> returnType)
|
||||
{
|
||||
if (Value.class.isAssignableFrom(returnType))
|
||||
return (OutputConverter<T>) VALUE;
|
||||
returnType = (Class<T>) ClassUtils.primitiveToWrapper(returnType); // wrapper holds same generic as primitive: wrapped
|
||||
return (OutputConverter<T>) Objects.requireNonNull(byResult.get(returnType),
|
||||
"Unregistered output type: " + returnType + ". Register in OutputConverter");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts the given input object into a {@link LazyValue}, to be used in return values of Scarpet functions</p>
|
||||
*
|
||||
* <p>Returns {@link LazyValue#NULL} if passed a {@code null} input</p>
|
||||
*
|
||||
* @param input The value to convert
|
||||
* @return The converted value
|
||||
*/
|
||||
public LazyValue convert(T input)
|
||||
{
|
||||
return input == null ? LazyValue.NULL : converter.apply(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new type to be able to be used as the return value of methods, converting from inputType to a {@link LazyValue}.</p>
|
||||
*
|
||||
* @see #registerToValue(Class, Function)
|
||||
* @param <T> 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 <T> void register(Class<T> inputType, Function<T, LazyValue> converter)
|
||||
{
|
||||
OutputConverter<T> instance = new OutputConverter<T>(converter);
|
||||
if (byResult.containsKey(inputType))
|
||||
throw new IllegalArgumentException(inputType + " already has a registered OutputConverter");
|
||||
byResult.put(inputType, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @see #register(Class, Function)
|
||||
* @param <T> 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 <T> void registerToValue(Class<T> inputType, Function<T, Value> converter)
|
||||
{
|
||||
OutputConverter<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Class that holds annotations for Scarpet parameters.</p>
|
||||
*
|
||||
* @see Param.Strict
|
||||
* @see Param.AllowSingleton
|
||||
* @see Param.Custom
|
||||
* @see Locator.Block
|
||||
* @see Locator.Vec3d
|
||||
* @see Locator.Function
|
||||
*
|
||||
*/
|
||||
public interface Param
|
||||
{
|
||||
/**
|
||||
* <p>Determines that this parameter accepts being passing a value directly instead of a list of those values.</p>
|
||||
*
|
||||
* <p>Can only be used in {@link List} parameters.</p>
|
||||
*
|
||||
* <p>The function method will receive a singleton of the item in question if there's a single value.</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface AllowSingleton
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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()}).</p>
|
||||
*
|
||||
* <p>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)</p>
|
||||
*
|
||||
* <p><b>IMPORTANT:</b> 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.<br> Having it as {@code true} will also cause the function to be considered of variable arguments even if it doesn't have varargs.</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface KeyValuePairs
|
||||
{
|
||||
/**
|
||||
* <p>Whether or not this accepts the key-value pairs directly in the function call as myFunction(..., key, value, key2, value2)</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
boolean allowMultiparam() default true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Defines that the parameter's converter has to be retrieved from the custom converter storage, in order to allow extensions to register
|
||||
* <b>complex</b> {@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</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface Custom
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Defines that a parameter of type {@link String}, {@link Text}, {@link ServerPlayerEntity}, {@link Boolean} or other registered strict type
|
||||
* <b>must</b> be of its corresponding {@link Value} in order to be accepted (respectively {@link StringValue}, {@link FormattedTextValue},
|
||||
* {@link EntityValue} or {@link BooleanValue}).</p>
|
||||
*
|
||||
* <p>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()}.</p>
|
||||
*
|
||||
* <p>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}.</p>
|
||||
*
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface Strict
|
||||
{
|
||||
/**
|
||||
* <p>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}.</p>
|
||||
*
|
||||
* <p>Without shallow mode, it would only accept from specifically a {@link FormattedTextValue} or {@link BooleanValue} respectively.
|
||||
*
|
||||
* <p>Using this in an unsupported type will throw {@link IllegalArgumentException}, just as if you used the annotation in an unsupported
|
||||
* type.</p>
|
||||
*
|
||||
* <p>This is {@code false} by default.</p>
|
||||
*/
|
||||
boolean shallow() default false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Class that holds the actual converters and converter getting logic for those annotated types and things.</p>
|
||||
*
|
||||
* <p>It also holds the registry for strict {@link ValueConverter}s.</p>
|
||||
*
|
||||
* @see #registerStrictConverter(Class, boolean, ValueConverter)
|
||||
*
|
||||
*/
|
||||
public static final class Params
|
||||
{
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Public in order to allow custom {@link ValueConverter} to check whether values should be evaluated while testing conditions.</p>
|
||||
*/
|
||||
/*public static final ValueConverter<LazyValue> LAZY_VALUE_IDENTITY = new ValueConverter<LazyValue>()
|
||||
{ // 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<Value> lazyValueIterator, Context c, Context.Type theLazyT)
|
||||
{
|
||||
return lazyValueIterator.hasNext() ? lazyValueIterator.next() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "something";
|
||||
}
|
||||
};*/
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*/
|
||||
static final ValueConverter<Context> CONTEXT_PROVIDER = new ValueConverter<Context>()
|
||||
{
|
||||
@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<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int valueConsumption()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*/
|
||||
static final ValueConverter<Context.Type> CONTEXT_TYPE_PROVIDER = new ValueConverter<Context.Type>()
|
||||
{
|
||||
@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<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
return theLazyT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int valueConsumption()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* <p>Strict converters</p>
|
||||
*
|
||||
* <p>Stored as {@code <Pair<Type, shallow?>, Converter>}</p>
|
||||
*/
|
||||
private static Map<Pair<Class<?>, 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<Class<?>, 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new {@link Param.Strict} parameter converter with the specified shallowness.</p>
|
||||
*
|
||||
* <p>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}.</p>
|
||||
*
|
||||
* @param <T> 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 <T> void registerStrictConverter(Class<T> type, boolean shallow, ValueConverter<T> converter)
|
||||
{
|
||||
Pair<Class<?>, 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<BiFunction<AnnotatedType, Class<?>, ValueConverter<?>>> customFactories = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* <p>Allows extensions to register <b>COMPLEX</b> {@link ValueConverter} factories in order to be used with the {@link Param.Custom}
|
||||
* annotation.</p> <p><b>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.</b></p> <p>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.</p> <p>Factories are
|
||||
* expected to return {@code null} when the provided arguments don't match a {@link ValueConverter} they are able to create (or reuse).</p>
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @param <T> 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 <T> void registerCustomConverterFactory(BiFunction<AnnotatedType, Class<T>, ValueConverter<T>> factory)
|
||||
{
|
||||
customFactories.add((BiFunction<AnnotatedType, Class<?>, ValueConverter<?>>) (Object) factory);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // Stored correctly
|
||||
static <R> ValueConverter<R> getCustomConverter(AnnotatedType annoType, Class<R> type)
|
||||
{
|
||||
ValueConverter<R> result;
|
||||
for (BiFunction<AnnotatedType, Class<?>, ValueConverter<?>> factory : customFactories)
|
||||
{
|
||||
if ((result = (ValueConverter<R>) factory.apply(annoType, type)) != null)
|
||||
return result;
|
||||
}
|
||||
throw new IllegalArgumentException("No custom converter found for Param.Custom annotated param with type " + annoType.getType().getTypeName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Defines a method that can be used as a function in the Scarpet language.</p>
|
||||
*
|
||||
* <p>Methods annotated with this annotation are not required to accept and return the typical {@code Context context, Integer t, List<Value> 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.</p>
|
||||
*
|
||||
* <p>Types to be used in those functions must be already registered in their respective {@link ValueConverter} implementations.<br>
|
||||
* 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)}.<br>
|
||||
* In order to convert the output of your method to a {@link LazyValue} you will also need to register its conversion in {@link OutputConverter}</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Methods annotated with this annotation must not be static and must not declare throwing any checked exceptions.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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
|
||||
{
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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. <br>
|
||||
* The value, however, will be ignored if the function has a fixed number of parameters.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Use -1 to specify an unlimited number of parameters.</p>
|
||||
*
|
||||
* @return The maximum number of parameters this function can accept
|
||||
*/
|
||||
int maxParams() default -2;
|
||||
|
||||
/**
|
||||
* <p>Defines the Context Type that will be used when evaluating arguments to annotated methods.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Defaults to {@link Context.Type#NONE}, like any regular ContextFunctions</p>
|
||||
*
|
||||
* @see Context
|
||||
*/
|
||||
Context.Type contextType() default Context.Type.NONE;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>A simple {@link ValueConverter} implementation that converts from a specified subclass of {@link Value} into {@code <R>} by using a given
|
||||
* function.</p>
|
||||
*
|
||||
* <p>{@link SimpleTypeConverter}s are reused whenever asked for one, since they don't have any complexity.</p>
|
||||
*
|
||||
* @see #registerType(Class, Class, Function)
|
||||
*
|
||||
* @param <T> The type of the required input {@link Value}
|
||||
* @param <R> The type that this converter converts to
|
||||
*/
|
||||
public final class SimpleTypeConverter<T extends Value, R> implements ValueConverter<R>
|
||||
{
|
||||
private static final Map<Class<?>, SimpleTypeConverter<? extends Value, ?>> 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<T, R> converter;
|
||||
private final Class<T> valueClass;
|
||||
private final String typeName;
|
||||
|
||||
/**
|
||||
* <p>The default constructor for {@link SimpleTypeConverter}.</p>
|
||||
*
|
||||
* <p>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}</p> <p>Use {@link #registerType(Class, Class, Function)} for
|
||||
* that.</p>
|
||||
*
|
||||
* @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<T> inputType, Function<T, R> 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 <R> 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 <R> SimpleTypeConverter<Value, R> get(Class<R> outputType)
|
||||
{
|
||||
return (SimpleTypeConverter<Value, R>) byResult.get(outputType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R convert(Value value)
|
||||
{
|
||||
return valueClass.isInstance(value) ? converter.apply(valueClass.cast(value)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new conversion from a {@link Value} subclass to a Java type.</p>
|
||||
*
|
||||
* @param <T> The {@link Value} subtype required for this conversion, for automatic checked casting
|
||||
* @param <R> The type of the resulting object
|
||||
* @param requiredInputType The {@link Class} of {@code <T>}
|
||||
* @param outputType The {@link Class} of {@code <R>>}
|
||||
* @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 <R>}, 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 <T extends Value, R> void registerType(Class<T> requiredInputType, Class<R> outputType,
|
||||
Function<T, R> converter, String typeName)
|
||||
{
|
||||
SimpleTypeConverter<T, R> type = new SimpleTypeConverter<>(requiredInputType, converter, typeName);
|
||||
byResult.put(outputType, type);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>{@link ValueCaster}s are reused whenever asked for one, since they don't have any complexity.</p>
|
||||
*
|
||||
* @see #register(Class, String)
|
||||
*
|
||||
* @param <R> The {@link Value} subclass this {@link ValueCaster} casts to
|
||||
*/
|
||||
public final class ValueCaster<R> implements ValueConverter<R> // R always extends Value, not explicitly because of type checking
|
||||
{
|
||||
private static final Map<Class<? extends Value>, ValueCaster<? extends Value>> 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<R> outputType;
|
||||
private final String typeName;
|
||||
|
||||
private ValueCaster(Class<R> outputType, String typeName)
|
||||
{
|
||||
this.outputType = outputType;
|
||||
this.typeName = typeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return typeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the registered {@link ValueCaster} for the specified outputType.</p>
|
||||
*
|
||||
* @param <R> 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 <R> ValueCaster<R> get(Class<R> outputType)
|
||||
{
|
||||
return (ValueCaster<R>) byResult.get(outputType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R convert(Value value)
|
||||
{
|
||||
if (!outputType.isInstance(value))
|
||||
return null;
|
||||
return outputType.cast(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new {@link Value} to be able to use it in {@link SimpleTypeConverter}</p>
|
||||
*
|
||||
* @param <R> 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<!--, with the form //Outdated concept <code>(function name) requires a (typeName) to be passed as (argName, if
|
||||
* available)</code>-->
|
||||
*/
|
||||
public static <R extends Value> void register(Class<R> valueClass, String typeName)
|
||||
{
|
||||
ValueCaster<R> caster = new ValueCaster<R>(valueClass, typeName);
|
||||
byResult.putIfAbsent(valueClass, caster);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>Classes implementing this interface are able to convert {@link Value} instances into {@code <R>}, in order to easily use them in parameters for
|
||||
* Scarpet functions created using the {@link ScarpetFunction} annotation.</p>
|
||||
*
|
||||
* @param <R> The result type that the passed {@link Value}s will be converted to
|
||||
*/
|
||||
public interface ValueConverter<R>
|
||||
{
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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();
|
||||
|
||||
/**
|
||||
* <p>Converts the given {@link Value} to {@code <R>}, which was defined when being registered.</p>
|
||||
*
|
||||
* <p> 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)</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @param value The {@link Value} to convert
|
||||
* @return The converted value, or {@code null} if the conversion failed in the process
|
||||
* @apiNote <p>While most implementations of this method should and will return the type from this method, implementations that <b>require</b>
|
||||
* 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}.</p>
|
||||
* <p>Currently, the only implementations requiring that are {@link Params#CONTEXT_PROVIDER} and {@link Params#CONTEXT_TYPE_PROVIDER}</p>
|
||||
* <p>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.</p>
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@Nullable
|
||||
public R convert(Value value);
|
||||
|
||||
/**
|
||||
* <p>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)}.</p>
|
||||
*
|
||||
* @implNote The default implementation returns {@code false}
|
||||
* @see #valueConsumption()
|
||||
*/
|
||||
default public boolean consumesVariableArgs()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Declares the number of {@link Value}s this converter consumes from the {@link Iterator} passed to it in
|
||||
* {@link #checkAndConvert(Iterator, Context, Context.Type)}.</p>
|
||||
*
|
||||
* <p>If this {@link ValueConverter} can accept a variable number of arguments (therefore the result of calling {@link #consumesVariableArgs()}
|
||||
* <b>must</b> return {@code true}), it will return the minimum number of arguments it will consume.</p>
|
||||
*
|
||||
* @implNote The default implementation returns {@code 1}
|
||||
*
|
||||
*/
|
||||
default public int valueConsumption()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the proper {@link ValueConverter} for the given {@link AnnotatedType}, considering the type of {@code R[]} as {@code R}.</p>
|
||||
*
|
||||
* <p>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}.</p>
|
||||
*
|
||||
* <p>Some processing is delegated to the appropriate implementations of {@link ValueConverter} in order to get registered converters or generate
|
||||
* specific ones for some functions.</p>
|
||||
*
|
||||
* @param <R> 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 <R>}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <R> ValueConverter<R> fromAnnotatedType(AnnotatedType annoType)
|
||||
{
|
||||
Class<R> type = annoType.getType() instanceof ParameterizedType ? // We are defining R here.
|
||||
(Class<R>) ((ParameterizedType) annoType.getType()).getRawType() :
|
||||
(Class<R>) 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<String, String>... name)
|
||||
// Those will just fail with a ClassCastException.
|
||||
if (type.isArray())
|
||||
type = (Class<R>) type.getComponentType(); // Varargs
|
||||
type = (Class<R>) ClassUtils.primitiveToWrapper(type); // It will be boxed anyway, this saves unboxing-boxing
|
||||
if (type == List.class)
|
||||
return (ValueConverter<R>) ListConverter.fromAnnotatedType(annoType); // Already checked that type is List
|
||||
if (type == Map.class)
|
||||
return (ValueConverter<R>) MapConverter.fromAnnotatedType(annoType); // Already checked that type is Map
|
||||
if (type == Optional.class)
|
||||
return (ValueConverter<R>) 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<R>) 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<R>) Params.LAZY_VALUE_IDENTITY;
|
||||
if (type == Context.class)
|
||||
return (ValueConverter<R>) Params.CONTEXT_PROVIDER;
|
||||
if (type == Context.Type.class)
|
||||
return (ValueConverter<R>) Params.CONTEXT_TYPE_PROVIDER;
|
||||
return Objects.requireNonNull(SimpleTypeConverter.get(type), "Type " + type + " is not registered. Register it in SimpleTypeConverter to use it");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>This should be the preferred way to call the converter, since it <!--allows some conversions that may not be supported by using directly a
|
||||
* {@link Value},--> allows multi-param converters and allows meta converters (such as the {@link Context} provider)</p>
|
||||
*
|
||||
* @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}. <p>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()}</p> <p>This method holds the same nullability
|
||||
* constraints as {@link #convert(Value)}</p>
|
||||
* @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 <R>} 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<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
if (!valueIterator.hasNext())
|
||||
return null;
|
||||
return convert(valueIterator.next());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user