Skip to content

Commit

Permalink
feat: Enums in JVM (#2842)
Browse files Browse the repository at this point in the history
For #2514 

Tasks: 
- [x] Test cases for all enum variants
- [x] Generate Java classes for enums in schema
- [x] Serialize/deserialise enums for JVM verb server
- [x] Serialize/deserialise enums for JVM verb clients
- [x] Generate schema for enums in Java code
- ~~Generate schema a nd classes for enums in Kotlin code~~ #3062
- ~~Update comment extraction code to include enums~~ #3061
  • Loading branch information
tomdaffurn authored Oct 9, 2024
1 parent e25fcf7 commit b0465e3
Show file tree
Hide file tree
Showing 55 changed files with 1,456 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.ArrayList;
import java.util.List;

import org.jboss.logging.Logger;

import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -16,12 +18,14 @@

public class DatasourceProcessor {

private static final Logger log = Logger.getLogger(DatasourceProcessor.class);

@BuildStep
public SchemaContributorBuildItem registerDatasources(
List<JdbcDataSourceBuildItem> datasources,
BuildProducer<SystemPropertyBuildItem> systemPropProducer,
BuildProducer<GeneratedResourceBuildItem> generatedResourceBuildItemBuildProducer) {

log.infof("Processing %d datasource annotations into decls", datasources.size());
List<Decl> decls = new ArrayList<>();
List<String> namedDatasources = new ArrayList<>();
for (var ds : datasources) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
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<PrimitiveType.Primitive> INT_TYPES = Set.of(INT, LONG, BYTE, SHORT);

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index, FTLRecorder recorder) {
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);
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<Decl> extractEnumDecls(CombinedIndexBuildItem index, Collection<AnnotationInstance> enumAnnotations,
FTLRecorder recorder, ModuleBuilder moduleBuilder)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
List<Decl> 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));
}
} else {
var typeEnum = extractTypeEnum(index, moduleBuilder, classInfo, exported);
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)
throws NoSuchFieldException, IllegalAccessException {
Enum.Builder enumBuilder = Enum.newBuilder()
.setName(classInfo.simpleName())
.setExport(exported);
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<Class<?>> variantClasses) {
}

/**
* Type Enums are an interface with 1+ implementing classes. The classes may be: </br>
* - a wrapper for a FTL native type e.g. string, [string]. Has @EnumHolder annotation </br>
* - a class with arbitrary fields </br>
*/
private TypeEnum extractTypeEnum(CombinedIndexBuildItem index, ModuleBuilder moduleBuilder,
ClassInfo classInfo, boolean exported) throws ClassNotFoundException {
Enum.Builder enumBuilder = Enum.newBuilder()
.setName(classInfo.simpleName())
.setExport(exported);
var variants = index.getComputingIndex().getAllKnownImplementors(classInfo.name());
if (variants.isEmpty()) {
throw new RuntimeException("No variants found for enum: " + enumBuilder.getName());
}
var variantClasses = new ArrayList<Class<?>>();
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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

import xyz.block.ftl.Config;
import xyz.block.ftl.Cron;
import xyz.block.ftl.Enum;
import xyz.block.ftl.EnumHolder;
import xyz.block.ftl.Export;
import xyz.block.ftl.GeneratedRef;
import xyz.block.ftl.LeaseClient;
import xyz.block.ftl.Secret;
import xyz.block.ftl.Subscription;
Expand All @@ -21,10 +24,13 @@ private FTLDotNames() {
public static final DotName SECRET = DotName.createSimple(Secret.class);
public static final DotName CONFIG = DotName.createSimple(Config.class);
public static final DotName EXPORT = DotName.createSimple(Export.class);
public static final DotName ENUM = DotName.createSimple(Enum.class);
public static final DotName ENUM_HOLDER = DotName.createSimple(EnumHolder.class);
public static final DotName VERB = DotName.createSimple(Verb.class);
public static final DotName CRON = DotName.createSimple(Cron.class);
public static final DotName TYPE_ALIAS_MAPPER = DotName.createSimple(TypeAliasMapper.class);
public static final DotName TYPE_ALIAS = DotName.createSimple(TypeAlias.class);
public static final DotName SUBSCRIPTION = DotName.createSimple(Subscription.class);
public static final DotName LEASE_CLIENT = DotName.createSimple(LeaseClient.class);
public static final DotName GENERATED_REF = DotName.createSimple(GeneratedRef.class);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
import java.util.stream.Stream;

import org.eclipse.microprofile.config.Config;
import org.jboss.logging.Logger;

import io.quarkus.bootstrap.prebuild.CodeGenException;
import io.quarkus.deployment.CodeGenContext;
import io.quarkus.deployment.CodeGenProvider;
import xyz.block.ftl.v1.schema.Data;
import xyz.block.ftl.v1.schema.Enum;
import xyz.block.ftl.v1.schema.EnumVariant;
import xyz.block.ftl.v1.schema.Module;
import xyz.block.ftl.v1.schema.Topic;
import xyz.block.ftl.v1.schema.Type;
Expand All @@ -26,6 +28,7 @@ public abstract class JVMCodeGenerator implements CodeGenProvider {

public static final String PACKAGE_PREFIX = "ftl.";
public static final String TYPE_MAPPER = "TypeAliasMapper";
private static final Logger log = Logger.getLogger(JVMCodeGenerator.class);

@Override
public String providerId() {
Expand All @@ -39,12 +42,14 @@ public String inputDirectory() {

@Override
public boolean trigger(CodeGenContext context) throws CodeGenException {
log.info("Generating JVM clients, data, enums from schema");
if (!Files.isDirectory(context.inputDir())) {
return false;
}
List<Module> modules = new ArrayList<>();
Map<DeclRef, Type> typeAliasMap = new HashMap<>();
Map<DeclRef, String> nativeTypeAliasMap = new HashMap<>();
Map<DeclRef, List<EnumInfo>> enumVariantInfoMap = new HashMap<>();
try (Stream<Path> pathStream = Files.list(context.inputDir())) {
for (var file : pathStream.toList()) {
String fileName = file.getFileName().toString();
Expand Down Expand Up @@ -109,14 +114,16 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
if (!data.getExport()) {
continue;
}
generateDataObject(module, data, packageName, typeAliasMap, nativeTypeAliasMap, context.outDir());
generateDataObject(module, data, packageName, typeAliasMap, nativeTypeAliasMap, enumVariantInfoMap,
context.outDir());

} else if (decl.hasEnum()) {
var data = decl.getEnum();
if (!data.getExport()) {
continue;
}
generateEnum(module, data, packageName, typeAliasMap, nativeTypeAliasMap, context.outDir());
generateEnum(module, data, packageName, typeAliasMap, nativeTypeAliasMap, enumVariantInfoMap,
context.outDir());
} else if (decl.hasTopic()) {
var data = decl.getTopic();
if (!data.getExport()) {
Expand All @@ -141,10 +148,12 @@ protected abstract void generateTopicSubscription(Module module, Topic data, Str
Map<DeclRef, Type> typeAliasMap, Map<DeclRef, String> nativeTypeAliasMap, Path outputDir) throws IOException;

protected abstract void generateEnum(Module module, Enum data, String packageName, Map<DeclRef, Type> typeAliasMap,
Map<DeclRef, String> nativeTypeAliasMap, Path outputDir) throws IOException;
Map<DeclRef, String> nativeTypeAliasMap, Map<DeclRef, List<EnumInfo>> enumVariantInfoMap, Path outputDir)
throws IOException;

protected abstract void generateDataObject(Module module, Data data, String packageName, Map<DeclRef, Type> typeAliasMap,
Map<DeclRef, String> nativeTypeAliasMap, Path outputDir) throws IOException;
Map<DeclRef, String> nativeTypeAliasMap, Map<DeclRef, List<EnumInfo>> enumVariantInfoMap, Path outputDir)
throws IOException;

protected abstract void generateVerb(Module module, Verb verb, String packageName, Map<DeclRef, Type> typeAliasMap,
Map<DeclRef, String> nativeTypeAliasMap, Path outputDir) throws IOException;
Expand All @@ -157,6 +166,9 @@ public boolean shouldRun(Path sourceDir, Config config) {
public record DeclRef(String module, String name) {
}

public record EnumInfo(String interfaceType, EnumVariant variant, List<EnumVariant> otherVariants) {
}

protected static String className(String in) {
return Character.toUpperCase(in.charAt(0)) + in.substring(1);
}
Expand Down
Loading

0 comments on commit b0465e3

Please sign in to comment.