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
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user