All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xyz.block.ftl.deployment.EnumProcessor Maven / Gradle / Ivy

There is a newer version: 0.415.1
Show newest version
package xyz.block.ftl.deployment;

import static org.jboss.jandex.PrimitiveType.Primitive.BYTE;
import static org.jboss.jandex.PrimitiveType.Primitive.INT;
import static org.jboss.jandex.PrimitiveType.Primitive.LONG;
import static org.jboss.jandex.PrimitiveType.Primitive.SHORT;
import static xyz.block.ftl.deployment.FTLDotNames.ENUM_HOLDER;
import static xyz.block.ftl.deployment.FTLDotNames.GENERATED_REF;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.PrimitiveType;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import xyz.block.ftl.runtime.FTLRecorder;
import xyz.block.ftl.v1.schema.Decl;
import xyz.block.ftl.v1.schema.Enum;
import xyz.block.ftl.v1.schema.EnumVariant;
import xyz.block.ftl.v1.schema.Int;
import xyz.block.ftl.v1.schema.IntValue;
import xyz.block.ftl.v1.schema.StringValue;
import xyz.block.ftl.v1.schema.TypeValue;
import xyz.block.ftl.v1.schema.Value;

public class EnumProcessor {

    private static final Logger log = Logger.getLogger(EnumProcessor.class);
    public static final Set INT_TYPES = Set.of(INT, LONG, BYTE, SHORT);

    @BuildStep
    @Record(ExecutionTime.RUNTIME_INIT)
    SchemaContributorBuildItem handleEnums(
            CombinedIndexBuildItem index,
            FTLRecorder recorder,
            CommentsBuildItem commentsBuildItem) {
        var enumAnnotations = index.getIndex().getAnnotations(FTLDotNames.ENUM);
        log.infof("Processing %d enum annotations into decls", enumAnnotations.size());
        return new SchemaContributorBuildItem(moduleBuilder -> {
            try {
                var decls = extractEnumDecls(index, enumAnnotations, recorder, moduleBuilder, commentsBuildItem);
                for (var decl : decls) {
                    moduleBuilder.addDecls(decl);
                }
            } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * Extract all enums for this module, returning a Decl for each. Also registers the enums with the recorder, which
     * sets up Jackson serialization in the runtime.
     * ModuleBuilder.buildType is used, and has the side effect of adding child Decls to the module.
     */
    private List extractEnumDecls(CombinedIndexBuildItem index, Collection enumAnnotations,
            FTLRecorder recorder, ModuleBuilder moduleBuilder, CommentsBuildItem commentsBuildItem)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        List decls = new ArrayList<>();
        for (var enumAnnotation : enumAnnotations) {
            boolean exported = enumAnnotation.target().hasAnnotation(FTLDotNames.EXPORT);
            ClassInfo classInfo = enumAnnotation.target().asClass();
            Class clazz = Class.forName(classInfo.name().toString(), false,
                    Thread.currentThread().getContextClassLoader());
            var isLocalToModule = !classInfo.hasDeclaredAnnotation(GENERATED_REF);

            if (classInfo.isEnum()) {
                // Value enum
                recorder.registerEnum(clazz);
                if (isLocalToModule) {
                    decls.add(extractValueEnum(classInfo, clazz, exported, commentsBuildItem));
                }
            } else {
                var typeEnum = extractTypeEnum(index, moduleBuilder, classInfo, exported, commentsBuildItem);
                recorder.registerEnum(clazz, typeEnum.variantClasses);
                if (isLocalToModule) {
                    decls.add(typeEnum.decl);
                }
            }
        }
        return decls;
    }

    /**
     * Value enums are Java language enums with a single field 'value'
     */
    private Decl extractValueEnum(ClassInfo classInfo, Class clazz, boolean exported, CommentsBuildItem commentsBuildItem)
            throws NoSuchFieldException, IllegalAccessException {
        String name = classInfo.simpleName();
        Enum.Builder enumBuilder = Enum.newBuilder()
                .setName(name)
                .setPos(PositionUtils.forClass(classInfo.name().toString()))
                .setExport(exported)
                .addAllComments(commentsBuildItem.getComments(name));
        FieldInfo valueField = classInfo.field("value");
        if (valueField == null) {
            throw new RuntimeException("Enum must have a 'value' field: " + classInfo.name());
        }
        Type type = valueField.type();
        xyz.block.ftl.v1.schema.Type.Builder typeBuilder = xyz.block.ftl.v1.schema.Type.newBuilder();
        if (isInt(type)) {
            typeBuilder.setInt(Int.newBuilder().build()).build();
        } else if (type.name().equals(DotName.STRING_NAME)) {
            typeBuilder.setString(xyz.block.ftl.v1.schema.String.newBuilder().build());
        } else {
            throw new RuntimeException(
                    "Enum value type must be String, int, long, short, or byte: " + classInfo.name());
        }
        enumBuilder.setType(typeBuilder.build());

        for (var constant : clazz.getEnumConstants()) {
            Field value = constant.getClass().getDeclaredField("value");
            value.setAccessible(true);
            Value.Builder valueBuilder = Value.newBuilder();
            if (isInt(type)) {
                long aLong = value.getLong(constant);
                valueBuilder.setIntValue(IntValue.newBuilder().setValue(aLong).build());
            } else {
                String aString = (String) value.get(constant);
                valueBuilder.setStringValue(StringValue.newBuilder().setValue(aString).build());
            }
            EnumVariant variant = EnumVariant.newBuilder()
                    .setName(constant.toString())
                    .setValue(valueBuilder)
                    .build();
            enumBuilder.addVariants(variant);
        }
        return Decl.newBuilder().setEnum(enumBuilder).build();
    }

    private record TypeEnum(Decl decl, List> variantClasses) {
    }

    /**
     * Type Enums are an interface with 1+ implementing classes. The classes may be: 
* - a wrapper for a FTL native type e.g. string, [string]. Has @EnumHolder annotation
* - a class with arbitrary fields
*/ private TypeEnum extractTypeEnum(CombinedIndexBuildItem index, ModuleBuilder moduleBuilder, ClassInfo classInfo, boolean exported, CommentsBuildItem commentsBuildItem) throws ClassNotFoundException { String name = classInfo.simpleName(); Enum.Builder enumBuilder = Enum.newBuilder() .setName(name) .setPos(PositionUtils.forClass(classInfo.name().toString())) .setExport(exported) .addAllComments(commentsBuildItem.getComments(name)); var variants = index.getComputingIndex().getAllKnownImplementors(classInfo.name()); if (variants.isEmpty()) { throw new RuntimeException("No variants found for enum: " + enumBuilder.getName()); } var variantClasses = new ArrayList>(); for (var variant : variants) { Type variantType; if (variant.hasAnnotation(ENUM_HOLDER)) { // Enum value holder class FieldInfo valueField = variant.field("value"); if (valueField == null) { throw new RuntimeException("Enum variant must have a 'value' field: " + variant.name()); } variantType = valueField.type(); // TODO add to variantClasses; write serialization code for holder classes } else { // Class is the enum variant type variantType = ClassType.builder(variant.name()).build(); Class variantClazz = Class.forName(variantType.name().toString(), false, Thread.currentThread().getContextClassLoader()); variantClasses.add(variantClazz); } xyz.block.ftl.v1.schema.Type declType = moduleBuilder.buildType(variantType, exported, Nullability.NOT_NULL); TypeValue typeValue = TypeValue.newBuilder().setValue(declType).build(); EnumVariant.Builder variantBuilder = EnumVariant.newBuilder() .setName(variant.simpleName()) .setValue(Value.newBuilder().setTypeValue(typeValue).build()); enumBuilder.addVariants(variantBuilder.build()); } return new TypeEnum(Decl.newBuilder().setEnum(enumBuilder).build(), variantClasses); } private boolean isInt(Type type) { return type.kind() == Type.Kind.PRIMITIVE && INT_TYPES.contains(type.asPrimitiveType().primitive()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy