Files
fabric-carpet/src/main/java/carpet/script/annotation/Param.java
T
2023-02-08 21:13:15 +01:00

306 lines
16 KiB
Java

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 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.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import javax.annotation.Nullable;
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 })
@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 })
@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 })
@interface Custom
{
}
/**
* <p>Defines that a parameter of type {@link String}, {@link Component}, {@link ServerPlayer}, {@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 Component} parameter or a {@link NumericValue} to a {@link BooleanValue}, but not any {@link Value}.</p>
*
*/
@Documented
@Retention(RUNTIME)
@Target({ PARAMETER, TYPE_USE })
@interface Strict
{
/**
* <p>Defines whether this parameter can accept types with "shallow strictness", that is, in order to get a {@link Component}, 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 and custom {@link ValueConverter}s.</p>
*
* @see #registerStrictConverter(Class, boolean, ValueConverter)
* @see #registerCustomConverterFactory(BiFunction)
*
*/
final class Params
{
/**
* <p>A {@link ValueConverter} that outputs the {@link Context} in which the function has been called, and throws {@link UnsupportedOperationException} when trying to convert a {@link Value}
* directly.</p>
*/
static final ValueConverter<Context> CONTEXT_PROVIDER = new ValueConverter<>()
{
@Nullable
@Override
public String getTypeName()
{
return null;
}
@Override
public Context convert(Value value, @Nullable Context context)
{
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, or throws {@link UnsupportedOperationException} when trying to convert a {@link Value}
* directly.</p>
*/
static final ValueConverter<Context.Type> CONTEXT_TYPE_PROVIDER = new ValueConverter<>()
{
@Nullable
@Override
public String getTypeName()
{
return null;
}
@Override
public Context.Type convert(Value value, @Nullable Context context)
{
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;
}
};
record StrictConverterInfo(Class<?> type, boolean shallow) {}
private static final Map<StrictConverterInfo, ValueConverter<?>> strictParamsByClassAndShallowness = new HashMap<>();
static
{ // TODO Specify strictness in name?
registerStrictConverter(String.class, false, new SimpleTypeConverter<>(StringValue.class, StringValue::getString, "string"));
registerStrictConverter(Component.class, false, new SimpleTypeConverter<>(FormattedTextValue.class, FormattedTextValue::getText, "text"));
registerStrictConverter(Component.class, true, new SimpleTypeConverter<>(StringValue.class, FormattedTextValue::getTextByValue, "text"));
registerStrictConverter(ServerPlayer.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();
StrictConverterInfo key = new StrictConverterInfo(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)
{
StrictConverterInfo key = new StrictConverterInfo(type, shallow);
if (strictParamsByClassAndShallowness.containsKey(key))
{
throw new IllegalArgumentException(type + " already has a registered " + (shallow ? "" : "non-") + "shallow StrictConverter");
}
strictParamsByClassAndShallowness.put(key, converter);
}
private static final 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());
}
}
}