Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enums in JVM #2842

Merged
merged 39 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
30604d9
rename javamodule to javaclient
tomdaffurn Sep 25, 2024
1f7ec20
rename verbs to javaserver
tomdaffurn Sep 25, 2024
39db3ee
move comment test code to javaserver
tomdaffurn Sep 25, 2024
e7fcb2e
Generate enums in Java
tomdaffurn Sep 26, 2024
f1dda75
Handle a Decl used in multiple enums
tomdaffurn Sep 26, 2024
e03433a
Reused enums not working in Go. Fix in Java
tomdaffurn Sep 27, 2024
37b3a16
Write @Enum annotation into generated enums
tomdaffurn Sep 30, 2024
6520f9f
Write @Enum annotation into generated enums
tomdaffurn Sep 30, 2024
8d1ffae
Quarkus build processors log their task count
tomdaffurn Sep 30, 2024
4bd062d
Quarkus build processors log their task count
tomdaffurn Sep 30, 2024
5066685
Extract schema for value enums
tomdaffurn Sep 30, 2024
ef7668f
Merge branch 'main' into tom/jvm-enum-extract
tomdaffurn Sep 30, 2024
9fc1872
Validate enums field and type
tomdaffurn Oct 1, 2024
085784d
Properly handle enums. Validate schema name clashes
tomdaffurn Oct 1, 2024
15fc6b7
Clean up logs from processors
tomdaffurn Oct 1, 2024
8b1f40b
Extract schema from type enums
tomdaffurn Oct 2, 2024
2be4671
Extract schema from type enum holder classes
tomdaffurn Oct 2, 2024
401feab
merge from main; resolve conflicts
tomdaffurn Oct 2, 2024
fdfbc89
Extract schema from type enum holder classes
tomdaffurn Oct 2, 2024
fed496b
Serialise/deserialize value enums
tomdaffurn Oct 4, 2024
8836dec
Serialise/deserialize type enums
tomdaffurn Oct 8, 2024
319997c
update integration tests
tomdaffurn Oct 8, 2024
ea0108d
merge from main; resolve conflicts
tomdaffurn Oct 8, 2024
65e68b7
fix merge mistakes
tomdaffurn Oct 8, 2024
7f964c2
revert whitespace
tomdaffurn Oct 8, 2024
ee192a6
whitespace
tomdaffurn Oct 8, 2024
4ea9f38
whitespace
tomdaffurn Oct 8, 2024
63d3e64
Merge branch 'main' into tom/jvm-enum-extract
tomdaffurn Oct 9, 2024
361d491
Merge branch 'main' into tom/jvm-enum-extract
tomdaffurn Oct 9, 2024
65c4233
fix typealias exceptions
tomdaffurn Oct 9, 2024
a3395bd
fix accidental whitespace
tomdaffurn Oct 9, 2024
f7d1bba
refactor EnumProcessor for clarity
tomdaffurn Oct 9, 2024
b937adc
comments
tomdaffurn Oct 9, 2024
dc6b07e
fix errors
tomdaffurn Oct 9, 2024
16d77c2
comments
tomdaffurn Oct 9, 2024
9fd824f
Don't produce decls for type aliases in other modules
tomdaffurn Oct 9, 2024
959fd2e
fix integration tests
tomdaffurn Oct 9, 2024
b8b1b50
cleanup
tomdaffurn Oct 9, 2024
5f0f0a3
change to jboss logger
tomdaffurn Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
tomdaffurn marked this conversation as resolved.
Show resolved Hide resolved
import org.slf4j.LoggerFactory;

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

public class DatasourceProcessor {

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

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

log.info("Processing {} 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,195 @@
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 java.util.function.Consumer;

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.slf4j.Logger;
import org.slf4j.LoggerFactory;

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 = LoggerFactory.getLogger(EnumProcessor.class);
tomdaffurn marked this conversation as resolved.
Show resolved Hide resolved
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.info("Processing {} enum annotations into decls", enumAnnotations.size());

return new SchemaContributorBuildItem(new Consumer<ModuleBuilder>() {
@Override
public void accept(ModuleBuilder 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,15 @@
import java.util.stream.Stream;

import org.eclipse.microprofile.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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 +29,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 = LoggerFactory.getLogger(JVMCodeGenerator.class);
tomdaffurn marked this conversation as resolved.
Show resolved Hide resolved

@Override
public String providerId() {
Expand All @@ -39,12 +43,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 +115,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 +149,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 +167,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
Loading