From 30604d95ecdf76d5a1a9c57527956cf9fba1fb81 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 25 Sep 2024 14:42:56 +1000 Subject: [PATCH 01/34] rename javamodule to javaclient --- jvm-runtime/jvm_integration_test.go | 6 +++--- jvm-runtime/testdata/java/javaclient/ftl.toml | 2 ++ .../testdata/java/{javamodule => javaclient}/pom.xml | 2 +- .../src/main/java/xyz/block/ftl/test/DidMapper.java | 0 .../main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java | 0 jvm-runtime/testdata/java/javamodule/ftl.toml | 2 -- jvm-runtime/testdata/java/{verbs => javaserver}/ftl.toml | 0 jvm-runtime/testdata/java/{verbs => javaserver}/pom.xml | 0 .../src/main/java/xyz/block/ftl/test/Verbs.java | 0 9 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 jvm-runtime/testdata/java/javaclient/ftl.toml rename jvm-runtime/testdata/java/{javamodule => javaclient}/pom.xml (94%) rename jvm-runtime/testdata/java/{javamodule => javaclient}/src/main/java/xyz/block/ftl/test/DidMapper.java (100%) rename jvm-runtime/testdata/java/{javamodule => javaclient}/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java (100%) delete mode 100644 jvm-runtime/testdata/java/javamodule/ftl.toml rename jvm-runtime/testdata/java/{verbs => javaserver}/ftl.toml (100%) rename jvm-runtime/testdata/java/{verbs => javaserver}/pom.xml (100%) rename jvm-runtime/testdata/java/{verbs => javaserver}/src/main/java/xyz/block/ftl/test/Verbs.java (100%) diff --git a/jvm-runtime/jvm_integration_test.go b/jvm-runtime/jvm_integration_test.go index f378d3178e..079073a3b4 100644 --- a/jvm-runtime/jvm_integration_test.go +++ b/jvm-runtime/jvm_integration_test.go @@ -147,10 +147,10 @@ func TestJVMToGoCall(t *testing.T) { in.Run(t, in.WithJavaBuild(), in.CopyModuleWithLanguage("gomodule", "go"), - in.CopyModuleWithLanguage("javamodule", "java"), + in.CopyModuleWithLanguage("javaclient", "java"), in.CopyModuleWithLanguage("kotlinmodule", "kotlin"), in.Deploy("gomodule"), - in.Deploy("javamodule"), + in.Deploy("javaclient"), in.Deploy("kotlinmodule"), in.SubTests(tests...), ) @@ -175,7 +175,7 @@ func PairedTest(name string, testFunc func(module string) in.Action) []in.SubTes }, { Name: name + "-java", - Action: testFunc("javamodule"), + Action: testFunc("javaclient"), }, { Name: name + "-kotlin", diff --git a/jvm-runtime/testdata/java/javaclient/ftl.toml b/jvm-runtime/testdata/java/javaclient/ftl.toml new file mode 100644 index 0000000000..42c33899cd --- /dev/null +++ b/jvm-runtime/testdata/java/javaclient/ftl.toml @@ -0,0 +1,2 @@ +module = "javaclient" +language = "java" diff --git a/jvm-runtime/testdata/java/javamodule/pom.xml b/jvm-runtime/testdata/java/javaclient/pom.xml similarity index 94% rename from jvm-runtime/testdata/java/javamodule/pom.xml rename to jvm-runtime/testdata/java/javaclient/pom.xml index 31d53c8335..91784be876 100644 --- a/jvm-runtime/testdata/java/javamodule/pom.xml +++ b/jvm-runtime/testdata/java/javaclient/pom.xml @@ -2,7 +2,7 @@ 4.0.0 xyz.block.ftl.examples - javamodule + javaclient 1.0-SNAPSHOT diff --git a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/DidMapper.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/DidMapper.java similarity index 100% rename from jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/DidMapper.java rename to jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/DidMapper.java diff --git a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java similarity index 100% rename from jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java rename to jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java diff --git a/jvm-runtime/testdata/java/javamodule/ftl.toml b/jvm-runtime/testdata/java/javamodule/ftl.toml deleted file mode 100644 index 88d8a88c98..0000000000 --- a/jvm-runtime/testdata/java/javamodule/ftl.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "javamodule" -language = "java" diff --git a/jvm-runtime/testdata/java/verbs/ftl.toml b/jvm-runtime/testdata/java/javaserver/ftl.toml similarity index 100% rename from jvm-runtime/testdata/java/verbs/ftl.toml rename to jvm-runtime/testdata/java/javaserver/ftl.toml diff --git a/jvm-runtime/testdata/java/verbs/pom.xml b/jvm-runtime/testdata/java/javaserver/pom.xml similarity index 100% rename from jvm-runtime/testdata/java/verbs/pom.xml rename to jvm-runtime/testdata/java/javaserver/pom.xml diff --git a/jvm-runtime/testdata/java/verbs/src/main/java/xyz/block/ftl/test/Verbs.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/test/Verbs.java similarity index 100% rename from jvm-runtime/testdata/java/verbs/src/main/java/xyz/block/ftl/test/Verbs.java rename to jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/test/Verbs.java From 1f7ec20d58246aea30f28a09f6447fec3acbb842 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 25 Sep 2024 14:44:35 +1000 Subject: [PATCH 02/34] rename verbs to javaserver --- jvm-runtime/jvm_integration_test.go | 8 ++++---- jvm-runtime/testdata/java/javaserver/ftl.toml | 2 +- jvm-runtime/testdata/java/javaserver/pom.xml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/jvm-runtime/jvm_integration_test.go b/jvm-runtime/jvm_integration_test.go index 079073a3b4..7bd524dc4d 100644 --- a/jvm-runtime/jvm_integration_test.go +++ b/jvm-runtime/jvm_integration_test.go @@ -32,12 +32,12 @@ func TestLifecycleJVM(t *testing.T) { func TestVerbCalls(t *testing.T) { in.Run(t, in.WithLanguages("java"), - in.CopyModule("verbs"), - in.Deploy("verbs"), - in.Call("verbs", "anyInput", map[string]string{"name": "Jimmy"}, func(t testing.TB, response string) { + in.CopyModule("javaserver"), + in.Deploy("javaserver"), + in.Call("javaserver", "anyInput", map[string]string{"name": "Jimmy"}, func(t testing.TB, response string) { assert.Equal(t, "Jimmy", response) }), - in.Call("verbs", "anyOutput", "Jimmy", func(t testing.TB, response map[string]string) { + in.Call("javaserver", "anyOutput", "Jimmy", func(t testing.TB, response map[string]string) { assert.Equal(t, map[string]string{"name": "Jimmy"}, response) }), ) diff --git a/jvm-runtime/testdata/java/javaserver/ftl.toml b/jvm-runtime/testdata/java/javaserver/ftl.toml index f059634b9b..16dc208d35 100644 --- a/jvm-runtime/testdata/java/javaserver/ftl.toml +++ b/jvm-runtime/testdata/java/javaserver/ftl.toml @@ -1,2 +1,2 @@ -module = "verbs" +module = "javaserver" language = "java" diff --git a/jvm-runtime/testdata/java/javaserver/pom.xml b/jvm-runtime/testdata/java/javaserver/pom.xml index 17cc01f697..411a251bdf 100644 --- a/jvm-runtime/testdata/java/javaserver/pom.xml +++ b/jvm-runtime/testdata/java/javaserver/pom.xml @@ -2,7 +2,7 @@ 4.0.0 xyz.block.ftl.examples - verbs-module + javaserver 1.0-SNAPSHOT From 39db3ee749e385cf76d89588fb0b7e8c633686d7 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 25 Sep 2024 14:48:51 +1000 Subject: [PATCH 03/34] move comment test code to javaserver --- .../testdata/java/javacomments/ftl.toml | 2 -- .../testdata/java/javacomments/pom.xml | 22 ------------------- .../ftl/javacomments/CommentedModule.java | 0 .../xyz/block/ftl/javacomments/DataClass.java | 0 .../xyz/block/ftl/javacomments/EnumType.java | 0 5 files changed, 24 deletions(-) delete mode 100644 jvm-runtime/testdata/java/javacomments/ftl.toml delete mode 100644 jvm-runtime/testdata/java/javacomments/pom.xml rename jvm-runtime/testdata/java/{javacomments => javaserver}/src/main/java/xyz/block/ftl/javacomments/CommentedModule.java (100%) rename jvm-runtime/testdata/java/{javacomments => javaserver}/src/main/java/xyz/block/ftl/javacomments/DataClass.java (100%) rename jvm-runtime/testdata/java/{javacomments => javaserver}/src/main/java/xyz/block/ftl/javacomments/EnumType.java (100%) diff --git a/jvm-runtime/testdata/java/javacomments/ftl.toml b/jvm-runtime/testdata/java/javacomments/ftl.toml deleted file mode 100644 index ee2f93e1fb..0000000000 --- a/jvm-runtime/testdata/java/javacomments/ftl.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "javacomments" -language = "java" diff --git a/jvm-runtime/testdata/java/javacomments/pom.xml b/jvm-runtime/testdata/java/javacomments/pom.xml deleted file mode 100644 index f872bb7f7a..0000000000 --- a/jvm-runtime/testdata/java/javacomments/pom.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - 4.0.0 - xyz.block.ftl.examples - javacomments - 1.0-SNAPSHOT - - - xyz.block.ftl - ftl-build-parent-java - 1.0-SNAPSHOT - - - - - xyz.block - web5-dids - 2.0.1-debug1 - - - - diff --git a/jvm-runtime/testdata/java/javacomments/src/main/java/xyz/block/ftl/javacomments/CommentedModule.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/CommentedModule.java similarity index 100% rename from jvm-runtime/testdata/java/javacomments/src/main/java/xyz/block/ftl/javacomments/CommentedModule.java rename to jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/CommentedModule.java diff --git a/jvm-runtime/testdata/java/javacomments/src/main/java/xyz/block/ftl/javacomments/DataClass.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/DataClass.java similarity index 100% rename from jvm-runtime/testdata/java/javacomments/src/main/java/xyz/block/ftl/javacomments/DataClass.java rename to jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/DataClass.java diff --git a/jvm-runtime/testdata/java/javacomments/src/main/java/xyz/block/ftl/javacomments/EnumType.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/EnumType.java similarity index 100% rename from jvm-runtime/testdata/java/javacomments/src/main/java/xyz/block/ftl/javacomments/EnumType.java rename to jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/EnumType.java From e7fcb2e66ad65407865e3818bc2501f512d323bf Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Thu, 26 Sep 2024 16:07:15 +1000 Subject: [PATCH 04/34] Generate enums in Java --- .../ftl/deployment/JVMCodeGenerator.java | 17 +- .../deployment/JavaCodeGenerator.java | 193 +++++++++++++++--- .../deployment/KotlinCodeGenerator.java | 5 +- jvm-runtime/testdata/go/gomodule/server.go | 53 +++++ .../block/ftl/test/TestInvokeGoFromJava.java | 43 ++++ 5 files changed, 282 insertions(+), 29 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java index 60347f4cae..33ad9c6b20 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java @@ -17,6 +17,7 @@ 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; @@ -45,6 +46,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { List modules = new ArrayList<>(); Map typeAliasMap = new HashMap<>(); Map nativeTypeAliasMap = new HashMap<>(); + Map enumVariantInfoMap = new HashMap<>(); try (Stream pathStream = Files.list(context.inputDir())) { for (var file : pathStream.toList()) { String fileName = file.getFileName().toString(); @@ -99,14 +101,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()) { @@ -131,10 +135,12 @@ protected abstract void generateTopicSubscription(Module module, Topic data, Str Map typeAliasMap, Map nativeTypeAliasMap, Path outputDir) throws IOException; protected abstract void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Path outputDir) throws IOException; + Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + throws IOException; protected abstract void generateDataObject(Module module, Data data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Path outputDir) throws IOException; + Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + throws IOException; protected abstract void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, Map nativeTypeAliasMap, Path outputDir) throws IOException; @@ -147,6 +153,9 @@ public boolean shouldRun(Path sourceDir, Config config) { public record DeclRef(String module, String name) { } + public record EnumVariantInfo(String interfaceType, EnumVariant variant, List otherVariants) { + } + protected static String className(String in) { return Character.toUpperCase(in.charAt(0)) + in.substring(1); } diff --git a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java index c332343bdd..36776e31e2 100644 --- a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.stream.Collectors; import javax.lang.model.element.Modifier; @@ -37,9 +38,11 @@ import xyz.block.ftl.deployment.JVMCodeGenerator; 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; +import xyz.block.ftl.v1.schema.Value; import xyz.block.ftl.v1.schema.Verb; public class JavaCodeGenerator extends JVMCodeGenerator { @@ -99,35 +102,115 @@ protected void generateTopicSubscription(Module module, Topic data, String packa } protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Path outputDir) + Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) throws IOException { - String thisType = className(data.getName()); - TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType) - .addAnnotation( - AnnotationSpec.builder(GeneratedRef.class) - .addMember("name", "\"" + data.getName() + "\"") - .addMember("module", "\"" + module.getName() + "\"").build()) - .addModifiers(Modifier.PUBLIC); + String interfaceType = className(data.getName()); + if (data.hasType()) { + //Enums with a type are "value enums" - Java natively supports these + TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(interfaceType) + .addAnnotation(getGeneratedRefAnnotation(module.getName(), data.getName())) + .addModifiers(Modifier.PUBLIC); + + TypeName enumType = toAnnotatedJavaTypeName(data.getType(), typeAliasMap, nativeTypeAliasMap); + dataBuilder.addField(enumType, "value", Modifier.PRIVATE, Modifier.FINAL); + dataBuilder.addMethod(MethodSpec.constructorBuilder() + .addParameter(enumType, "value") + .addStatement("this.value = value") + .build()); + dataBuilder.addMethod(MethodSpec.methodBuilder("getValue") + .addModifiers(Modifier.PUBLIC) + .returns(enumType) + .addStatement("return value") + .build()); - for (var i : data.getVariantsList()) { - dataBuilder.addEnumConstant(i.getName()); - } + for (var i : data.getVariantsList()) { + Object value = toJavaValue(i.getValue()); + dataBuilder.addEnumConstant(i.getName(), + TypeSpec.anonymousClassBuilder("$L", value) + .build()); + } - JavaFile javaFile = JavaFile.builder(packageName, dataBuilder.build()) - .build(); + JavaFile javaFile = JavaFile.builder(packageName, dataBuilder.build()) + .build(); - javaFile.writeTo(outputDir); + javaFile.writeTo(outputDir); + } else { + // Enums without a type are (confusingly) "type enums". Java can't represent these directly, so we use a + // sealed class + + // TODO JavaPoet doesn't support 'sealed' or 'permits' syntax yet, so we can't seal the interface + // https://github.com/square/javapoet/issues/823 + TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder(interfaceType) + .addAnnotation(getGeneratedRefAnnotation(module.getName(), data.getName())) + .addModifiers(Modifier.PUBLIC); + + Map variantValuesTypes = data.getVariantsList().stream().collect( + Collectors.toMap(EnumVariant::getName, v -> toAnnotatedJavaTypeName(v.getValue().getTypeValue().getValue(), + typeAliasMap, nativeTypeAliasMap))); + for (var variant : data.getVariantsList()) { + // Interface has isX and getX methods for each variant + String name = variant.getName(); + TypeName valueTypeName = variantValuesTypes.get(name); + interfaceBuilder.addMethod(MethodSpec.methodBuilder("is" + name) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(TypeName.BOOLEAN) + .build()); + interfaceBuilder.addMethod(MethodSpec.methodBuilder("get" + name) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(valueTypeName) + .build()); + + if (variant.getValue().getTypeValue().getValue().hasRef()) { + // Value type is a Ref, so it will have a class generated by generateDataObject + // Store this variant in enumVariantInfoMap so we can fetch it later + enumVariantInfoMap.put(new DeclRef(module.getName(), name), + new EnumVariantInfo(interfaceType, variant, data.getVariantsList())); + } else { + // Value type isn't a Ref, so we make a wrapper class that implements our interface + TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(className(name)) + .addAnnotation(getGeneratedRefAnnotation(module.getName(), name)) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + dataBuilder.addField(valueTypeName, "value", Modifier.PRIVATE, Modifier.FINAL); + dataBuilder.addMethod(MethodSpec.constructorBuilder() + .addStatement("this.value = null") + .addModifiers(Modifier.PRIVATE) + .build()); + dataBuilder.addMethod(MethodSpec.constructorBuilder() + .addParameter(valueTypeName, "value") + .addStatement("this.value = value") + .addModifiers(Modifier.PUBLIC) + .build()); + addTypeEnumInterfaceMethods(packageName, interfaceType, dataBuilder, name, valueTypeName, + variantValuesTypes, false); + JavaFile javaFile = JavaFile.builder(packageName, dataBuilder.build()) + .build(); + javaFile.writeTo(outputDir); + } + JavaFile javaFile = JavaFile.builder(packageName, interfaceBuilder.build()) + .build(); + javaFile.writeTo(outputDir); + } + } } protected void generateDataObject(Module module, Data data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Path outputDir) throws IOException { + Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType) - .addAnnotation( - AnnotationSpec.builder(GeneratedRef.class) - .addMember("name", "\"" + data.getName() + "\"") - .addMember("module", "\"" + module.getName() + "\"").build()) + .addAnnotation(getGeneratedRefAnnotation(module.getName(), data.getName())) .addModifiers(Modifier.PUBLIC); + DeclRef key = new DeclRef(module.getName(), data.getName()); + if (enumVariantInfoMap.containsKey(key)) { + EnumVariantInfo enumVariantInfo = enumVariantInfoMap.get(key); + String name = enumVariantInfo.variant().getName(); + TypeName variantTypeName = ClassName.get(packageName, name); + Map variantValuesTypes = enumVariantInfo.otherVariants().stream().collect( + Collectors.toMap(EnumVariant::getName, v -> toAnnotatedJavaTypeName(v.getValue().getTypeValue().getValue(), + typeAliasMap, nativeTypeAliasMap))); + addTypeEnumInterfaceMethods(packageName, enumVariantInfo.interfaceType(), dataBuilder, name, + variantTypeName, variantValuesTypes, true); + } MethodSpec.Builder allConstructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); dataBuilder.addMethod(allConstructor.build()); @@ -197,13 +280,13 @@ protected void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, Map< } else if (type.hasString()) { return ClassName.get(String.class); } else if (type.hasOptional()) { - // Always box for optional, as normal primities can't be null + // Always box for optional, as normal primitives can't be null return toJavaTypeName(type.getOptional().getType(), typeAliasMap, nativeTypeAliasMap, true); } else if (type.hasRef()) { if (type.getRef().getModule().isEmpty()) { @@ -293,6 +376,70 @@ private TypeName toJavaTypeName(Type type, Map typeAliasMap, Map< throw new RuntimeException("Cannot generate Java type name: " + type); } + /** + * Get concrete value from a Value + */ + private Object toJavaValue(Value value) { + if (value.hasIntValue()) { + return value.getIntValue().getValue(); + } else if (value.hasStringValue()) { + return value.getStringValue().getValue(); + } else if (value.hasTypeValue()) { + // Can't instantiate a TypeValue now. Cannot happen because it's only used in type enums + throw new RuntimeException("Cannot generate TypeValue: " + value); + } + throw new RuntimeException("Cannot generate Java value: " + value); + } + + /** + * Adds the super interface and isX, getX methods to the dataBuilder for a type enum variant + */ + private static void addTypeEnumInterfaceMethods(String packageName, String interfaceType, TypeSpec.Builder dataBuilder, + String enumVariantName, TypeName variantTypeName, Map variantValuesTypes, boolean returnSelf) { + + dataBuilder.addSuperinterface(ClassName.get(packageName, interfaceType)); + + // Positive implementation of isX, getX for its type + dataBuilder.addMethod(MethodSpec.methodBuilder("is" + enumVariantName) + .addModifiers(Modifier.PUBLIC) + .returns(TypeName.BOOLEAN) + .addStatement("return true") + .build()); + + MethodSpec.Builder getMethod = MethodSpec.methodBuilder("get" + enumVariantName) + .addModifiers(Modifier.PUBLIC) + .returns(variantTypeName); + if (returnSelf) { + getMethod.addStatement("return this"); + } else { + getMethod.addStatement("return value"); + } + dataBuilder.addMethod(getMethod.build()); + + for (var thingIAmNot : variantValuesTypes.entrySet()) { + if (thingIAmNot.getKey().equals(enumVariantName)) { + continue; + } + // Negative implementation of isX, getX for other types + dataBuilder.addMethod(MethodSpec.methodBuilder("is" + thingIAmNot.getKey()) + .addModifiers(Modifier.PUBLIC) + .returns(TypeName.BOOLEAN) + .addStatement("return false") + .build()); + dataBuilder.addMethod(MethodSpec.methodBuilder("get" + thingIAmNot.getKey()) + .addModifiers(Modifier.PUBLIC) + .returns(thingIAmNot.getValue()) + .addStatement("throw new UnsupportedOperationException()") + .build()); + } + } + + private static @NotNull AnnotationSpec getGeneratedRefAnnotation(String module, String name) { + return AnnotationSpec.builder(GeneratedRef.class) + .addMember("name", "\"" + name + "\"") + .addMember("module", "\"" + module + "\"").build(); + } + protected static final Set JAVA_KEYWORDS = Set.of("abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", "else", "import", "public", "throws", "case", "enum", "instanceof", diff --git a/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java b/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java index 038d8f164e..e2369951da 100644 --- a/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java @@ -93,7 +93,7 @@ protected void generateTopicSubscription(Module module, Topic data, String packa } protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Path outputDir) + Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType) @@ -114,7 +114,8 @@ protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Path outputDir) throws IOException { + Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType) .addAnnotation( diff --git a/jvm-runtime/testdata/go/gomodule/server.go b/jvm-runtime/testdata/go/gomodule/server.go index 5fb717f6c9..3b6c398f93 100644 --- a/jvm-runtime/testdata/go/gomodule/server.go +++ b/jvm-runtime/testdata/go/gomodule/server.go @@ -39,6 +39,39 @@ type ParameterizedType[T any] struct { Map map[string]T } +//ftl:enum export +type ColorInt int + +const ( + Red ColorInt = 0 + Green ColorInt = 1 + Blue ColorInt = 2 +) + +//ftl:enum export +type TypeEnum interface{ typeEnum() } +type Scalar string +type StringList []string + +func (Scalar) typeEnum() {} +func (StringList) typeEnum() {} + +//ftl:enum +type Animal interface{ animal() } +type Cat struct{} +type Dog struct{} + +func (Cat) animal() {} +func (Dog) animal() {} + +//ftl:enum +type Mixed interface{ tag() } +type Word string +type Thing struct{} + +func (Word) tag() {} +func (Thing) tag() {} + //ftl:typealias //ftl:typemap kotlin "web5.sdk.dids.didcore.Did" type DID = did.DID @@ -193,3 +226,23 @@ func OptionalTestObjectOptionalFieldsVerb(ctx context.Context, val ftl.Option[Te func ExternalTypeVerb(ctx context.Context, did DID) (DID, error) { return did, nil } + +//ftl:verb export +func ValueEnumVerb(ctx context.Context, val ColorInt) (ColorInt, error) { + return val, nil +} + +//ftl:verb export +func TypeEnumVerb(ctx context.Context, val TypeEnum) (TypeEnum, error) { + return val, nil +} + +//ftl:verb export +func NoValueTypeEnumVerb(ctx context.Context, val Animal) (Animal, error) { + return val, nil +} + +//ftl:verb export +func MixedEnumVerb(ctx context.Context, val Mixed) (Mixed, error) { + return val, nil +} diff --git a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index 075b07b784..fdf4d6c193 100644 --- a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -6,13 +6,18 @@ import org.jetbrains.annotations.NotNull; +import ftl.gomodule.Animal; import ftl.gomodule.BoolVerbClient; import ftl.gomodule.BytesVerbClient; +import ftl.gomodule.ColorInt; import ftl.gomodule.EmptyVerbClient; import ftl.gomodule.ErrorEmptyVerbClient; import ftl.gomodule.ExternalTypeVerbClient; import ftl.gomodule.FloatVerbClient; import ftl.gomodule.IntVerbClient; +import ftl.gomodule.Mixed; +import ftl.gomodule.MixedEnumVerbClient; +import ftl.gomodule.NoValueTypeEnumVerbClient; import ftl.gomodule.ObjectArrayVerbClient; import ftl.gomodule.ObjectMapVerbClient; import ftl.gomodule.OptionalBoolVerbClient; @@ -27,9 +32,11 @@ import ftl.gomodule.OptionalTimeVerbClient; import ftl.gomodule.ParameterizedObjectVerbClient; import ftl.gomodule.ParameterizedType; +import ftl.gomodule.Scalar; import ftl.gomodule.SinkVerbClient; import ftl.gomodule.SourceVerbClient; import ftl.gomodule.StringArrayVerbClient; +import ftl.gomodule.StringList; import ftl.gomodule.StringMapVerbClient; import ftl.gomodule.StringVerbClient; import ftl.gomodule.TestObject; @@ -37,6 +44,9 @@ import ftl.gomodule.TestObjectOptionalFieldsVerbClient; import ftl.gomodule.TestObjectVerbClient; import ftl.gomodule.TimeVerbClient; +import ftl.gomodule.TypeEnum; +import ftl.gomodule.TypeEnumVerbClient; +import ftl.gomodule.ValueEnumVerbClient; import web5.sdk.dids.didcore.Did; import xyz.block.ftl.Export; import xyz.block.ftl.Verb; @@ -216,4 +226,37 @@ public Did externalTypeVerb(Did val, ExternalTypeVerbClient client) { return client.call(val); } + @Export + @Verb + public Animal noValueTypeEnumVerb(Animal animal, NoValueTypeEnumVerbClient client) { + if (animal.isCat()) { + return client.call(animal.getCat()); + } else { + return client.call(animal.getDog()); + } + } + + @Export + @Verb + public ColorInt valueEnumVerb(ColorInt color, ValueEnumVerbClient client) { + return client.call(ColorInt.Red); + } + + @Export + @Verb + public TypeEnum typeEnumVerb(TypeEnum value, TypeEnumVerbClient client) { + if (value.isScalar()) { + return client.call(new StringList(List.of("a", "b", "c"))); + } else if (value.isStringList()) { + return client.call(new Scalar("scalar")); + } else { + throw new IllegalArgumentException("unexpected value"); + } + } + + @Export + @Verb + public Mixed mixedEnumVerb(Mixed mixed, MixedEnumVerbClient client) { + return client.call(mixed); + } } From f1dda7542e71153a8b0c439fb68069c8ebb2f4f0 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Thu, 26 Sep 2024 16:36:30 +1000 Subject: [PATCH 05/34] Handle a Decl used in multiple enums --- .../ftl/deployment/JVMCodeGenerator.java | 8 +++--- .../deployment/JavaCodeGenerator.java | 26 ++++++++++--------- .../deployment/KotlinCodeGenerator.java | 4 +-- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java index 33ad9c6b20..dbf37d92d4 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java @@ -46,7 +46,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { List modules = new ArrayList<>(); Map typeAliasMap = new HashMap<>(); Map nativeTypeAliasMap = new HashMap<>(); - Map enumVariantInfoMap = new HashMap<>(); + Map> enumVariantInfoMap = new HashMap<>(); try (Stream pathStream = Files.list(context.inputDir())) { for (var file : pathStream.toList()) { String fileName = file.getFileName().toString(); @@ -135,11 +135,11 @@ protected abstract void generateTopicSubscription(Module module, Topic data, Str Map typeAliasMap, Map nativeTypeAliasMap, Path outputDir) throws IOException; protected abstract void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + Map nativeTypeAliasMap, Map> enumVariantInfoMap, Path outputDir) throws IOException; protected abstract void generateDataObject(Module module, Data data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + Map nativeTypeAliasMap, Map> enumVariantInfoMap, Path outputDir) throws IOException; protected abstract void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, @@ -153,7 +153,7 @@ public boolean shouldRun(Path sourceDir, Config config) { public record DeclRef(String module, String name) { } - public record EnumVariantInfo(String interfaceType, EnumVariant variant, List otherVariants) { + public record EnumInfo(String interfaceType, EnumVariant variant, List otherVariants) { } protected static String className(String in) { diff --git a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java index 36776e31e2..80528adedd 100644 --- a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java @@ -102,7 +102,7 @@ protected void generateTopicSubscription(Module module, Topic data, String packa } protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + Map nativeTypeAliasMap, Map> enumVariantInfoMap, Path outputDir) throws IOException { String interfaceType = className(data.getName()); if (data.hasType()) { @@ -163,8 +163,9 @@ protected void generateEnum(Module module, Enum data, String packageName, Map variantInfos = enumVariantInfoMap.computeIfAbsent(key, k -> List.of()); + variantInfos.add(new EnumInfo(interfaceType, variant, data.getVariantsList())); } else { // Value type isn't a Ref, so we make a wrapper class that implements our interface TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(className(name)) @@ -194,7 +195,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + Map nativeTypeAliasMap, Map> enumVariantInfoMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType) @@ -202,14 +203,15 @@ protected void generateDataObject(Module module, Data data, String packageName, .addModifiers(Modifier.PUBLIC); DeclRef key = new DeclRef(module.getName(), data.getName()); if (enumVariantInfoMap.containsKey(key)) { - EnumVariantInfo enumVariantInfo = enumVariantInfoMap.get(key); - String name = enumVariantInfo.variant().getName(); - TypeName variantTypeName = ClassName.get(packageName, name); - Map variantValuesTypes = enumVariantInfo.otherVariants().stream().collect( - Collectors.toMap(EnumVariant::getName, v -> toAnnotatedJavaTypeName(v.getValue().getTypeValue().getValue(), - typeAliasMap, nativeTypeAliasMap))); - addTypeEnumInterfaceMethods(packageName, enumVariantInfo.interfaceType(), dataBuilder, name, - variantTypeName, variantValuesTypes, true); + for (var enumVariantInfo: enumVariantInfoMap.get(key)) { + String name = enumVariantInfo.variant().getName(); + TypeName variantTypeName = ClassName.get(packageName, name); + Map variantValuesTypes = enumVariantInfo.otherVariants().stream().collect( + Collectors.toMap(EnumVariant::getName, v -> toAnnotatedJavaTypeName(v.getValue().getTypeValue().getValue(), + typeAliasMap, nativeTypeAliasMap))); + addTypeEnumInterfaceMethods(packageName, enumVariantInfo.interfaceType(), dataBuilder, name, + variantTypeName, variantValuesTypes, true); + } } MethodSpec.Builder allConstructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); diff --git a/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java b/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java index e2369951da..661846b1f3 100644 --- a/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/kotlin/deployment/src/main/java/xyz/block/ftl/kotlin/deployment/KotlinCodeGenerator.java @@ -93,7 +93,7 @@ protected void generateTopicSubscription(Module module, Topic data, String packa } protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + Map nativeTypeAliasMap, Map> enumVariantInfoMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType) @@ -114,7 +114,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Map nativeTypeAliasMap, Map enumVariantInfoMap, Path outputDir) + Map nativeTypeAliasMap, Map> enumVariantInfoMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType) From e03433aaf1dceac5792e9b9103f482e660e807f2 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Fri, 27 Sep 2024 11:12:44 +1000 Subject: [PATCH 06/34] Reused enums not working in Go. Fix in Java --- .../deployment/JavaCodeGenerator.java | 10 +++++---- jvm-runtime/testdata/go/gomodule/server.go | 22 +++++++++---------- .../block/ftl/test/TestInvokeGoFromJava.java | 12 +++++----- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java index 80528adedd..8b8cdcd9ef 100644 --- a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.nio.file.Path; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -164,7 +165,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map variantInfos = enumVariantInfoMap.computeIfAbsent(key, k -> List.of()); + List variantInfos = enumVariantInfoMap.computeIfAbsent(key, k -> new ArrayList<>()); variantInfos.add(new EnumInfo(interfaceType, variant, data.getVariantsList())); } else { // Value type isn't a Ref, so we make a wrapper class that implements our interface @@ -203,12 +204,13 @@ protected void generateDataObject(Module module, Data data, String packageName, .addModifiers(Modifier.PUBLIC); DeclRef key = new DeclRef(module.getName(), data.getName()); if (enumVariantInfoMap.containsKey(key)) { - for (var enumVariantInfo: enumVariantInfoMap.get(key)) { + for (var enumVariantInfo : enumVariantInfoMap.get(key)) { String name = enumVariantInfo.variant().getName(); TypeName variantTypeName = ClassName.get(packageName, name); Map variantValuesTypes = enumVariantInfo.otherVariants().stream().collect( - Collectors.toMap(EnumVariant::getName, v -> toAnnotatedJavaTypeName(v.getValue().getTypeValue().getValue(), - typeAliasMap, nativeTypeAliasMap))); + Collectors.toMap(EnumVariant::getName, + v -> toAnnotatedJavaTypeName(v.getValue().getTypeValue().getValue(), + typeAliasMap, nativeTypeAliasMap))); addTypeEnumInterfaceMethods(packageName, enumVariantInfo.interfaceType(), dataBuilder, name, variantTypeName, variantValuesTypes, true); } diff --git a/jvm-runtime/testdata/go/gomodule/server.go b/jvm-runtime/testdata/go/gomodule/server.go index 3b6c398f93..047cc46922 100644 --- a/jvm-runtime/testdata/go/gomodule/server.go +++ b/jvm-runtime/testdata/go/gomodule/server.go @@ -64,13 +64,13 @@ type Dog struct{} func (Cat) animal() {} func (Dog) animal() {} -//ftl:enum -type Mixed interface{ tag() } -type Word string -type Thing struct{} - -func (Word) tag() {} -func (Thing) tag() {} +//TODO this doesn't work yet: https://github.com/TBD54566975/ftl/issues/2857 +////ftl:enum +//type Mixed interface{ mixed() } +//type Word string +// +//func (Word) mixed() {} +//func (Dog) mixed() {} //ftl:typealias //ftl:typemap kotlin "web5.sdk.dids.didcore.Did" @@ -242,7 +242,7 @@ func NoValueTypeEnumVerb(ctx context.Context, val Animal) (Animal, error) { return val, nil } -//ftl:verb export -func MixedEnumVerb(ctx context.Context, val Mixed) (Mixed, error) { - return val, nil -} +////ftl:verb export +//func MixedEnumVerb(ctx context.Context, val Mixed) (Mixed, error) { +// return val, nil +//} diff --git a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index fdf4d6c193..5d03e0f97b 100644 --- a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -15,8 +15,6 @@ import ftl.gomodule.ExternalTypeVerbClient; import ftl.gomodule.FloatVerbClient; import ftl.gomodule.IntVerbClient; -import ftl.gomodule.Mixed; -import ftl.gomodule.MixedEnumVerbClient; import ftl.gomodule.NoValueTypeEnumVerbClient; import ftl.gomodule.ObjectArrayVerbClient; import ftl.gomodule.ObjectMapVerbClient; @@ -254,9 +252,9 @@ public TypeEnum typeEnumVerb(TypeEnum value, TypeEnumVerbClient client) { } } - @Export - @Verb - public Mixed mixedEnumVerb(Mixed mixed, MixedEnumVerbClient client) { - return client.call(mixed); - } + // @Export + // @Verb + // public Mixed mixedEnumVerb(Mixed mixed, MixedEnumVerbClient client) { + // return client.call(mixed); + // } } From 37b3a16352b8002f55b8a0794f6f88bf10cdd94c Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Mon, 30 Sep 2024 17:55:28 +1000 Subject: [PATCH 07/34] Write @Enum annotation into generated enums --- .../src/main/java/xyz/block/ftl/Enum.java | 14 +++++++ jvm-runtime/testdata/go/gomodule/server.go | 41 +++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Enum.java diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Enum.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Enum.java new file mode 100644 index 0000000000..a5966aa703 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Enum.java @@ -0,0 +1,14 @@ +package xyz.block.ftl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks an enum type + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface Enum { +} diff --git a/jvm-runtime/testdata/go/gomodule/server.go b/jvm-runtime/testdata/go/gomodule/server.go index 047cc46922..e002840602 100644 --- a/jvm-runtime/testdata/go/gomodule/server.go +++ b/jvm-runtime/testdata/go/gomodule/server.go @@ -48,6 +48,23 @@ const ( Blue ColorInt = 2 ) +type ColorWrapper struct { + Color ColorInt +} + +//ftl:enum export +type Shape string + +const ( + Circle Shape = "circle" + Square Shape = "square" + Triangle Shape = "triangle" +) + +type ShapeWrapper struct { + Shape Shape +} + //ftl:enum export type TypeEnum interface{ typeEnum() } type Scalar string @@ -56,6 +73,10 @@ type StringList []string func (Scalar) typeEnum() {} func (StringList) typeEnum() {} +type TypeEnumWrapper struct { + Type TypeEnum +} + //ftl:enum type Animal interface{ animal() } type Cat struct{} @@ -64,6 +85,10 @@ type Dog struct{} func (Cat) animal() {} func (Dog) animal() {} +type AnimalWrapper struct { + Animal Animal +} + //TODO this doesn't work yet: https://github.com/TBD54566975/ftl/issues/2857 ////ftl:enum //type Mixed interface{ mixed() } @@ -228,20 +253,30 @@ func ExternalTypeVerb(ctx context.Context, did DID) (DID, error) { } //ftl:verb export -func ValueEnumVerb(ctx context.Context, val ColorInt) (ColorInt, error) { +func ValueEnumVerb(ctx context.Context, val ColorWrapper) (ColorWrapper, error) { + return val, nil +} + +//ftl:verb export +func ShapeEnumVerb(ctx context.Context, val ShapeWrapper) (ShapeWrapper, error) { return val, nil } //ftl:verb export -func TypeEnumVerb(ctx context.Context, val TypeEnum) (TypeEnum, error) { +func TypeEnumVerb(ctx context.Context, val TypeEnumWrapper) (TypeEnumWrapper, error) { return val, nil } //ftl:verb export -func NoValueTypeEnumVerb(ctx context.Context, val Animal) (Animal, error) { +func NoValueTypeEnumVerb(ctx context.Context, val AnimalWrapper) (AnimalWrapper, error) { return val, nil } +//ftl:verb export +func GetAnimal(ctx context.Context) (AnimalWrapper, error) { + return AnimalWrapper{Animal: Cat{}}, nil +} + ////ftl:verb export //func MixedEnumVerb(ctx context.Context, val Mixed) (Mixed, error) { // return val, nil From 6520f9f8457ec3ad0f3deb7b3cc6771e089837fb Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Mon, 30 Sep 2024 19:44:25 +1000 Subject: [PATCH 08/34] Write @Enum annotation into generated enums --- .../block/ftl/javalang/deployment/JavaCodeGenerator.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java index 8b8cdcd9ef..05966c3d73 100644 --- a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java @@ -110,6 +110,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map variantValuesTypes = data.getVariantsList().stream().collect( From 8d1ffae91a2fbe7ed6a6293acc5bff96273bf92d Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Mon, 30 Sep 2024 19:46:20 +1000 Subject: [PATCH 09/34] Quarkus build processors log their task count --- .../block/ftl/deployment/DatasourceProcessor.java | 7 ++++++- .../xyz/block/ftl/deployment/JVMCodeGenerator.java | 4 ++++ .../xyz/block/ftl/deployment/ModuleProcessor.java | 8 +++++--- .../ftl/deployment/SubscriptionProcessor.java | 14 ++++++++++---- .../xyz/block/ftl/deployment/TopicsProcessor.java | 5 +++++ .../block/ftl/deployment/TypeAliasProcessor.java | 11 ++++++++++- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java index 0f3aaf767c..7d9db125f1 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java @@ -4,6 +4,9 @@ import java.util.ArrayList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -16,12 +19,14 @@ public class DatasourceProcessor { + private static final Logger log = LoggerFactory.getLogger(DatasourceProcessor.class); + @BuildStep public SchemaContributorBuildItem registerDatasources( List datasources, BuildProducer systemPropProducer, BuildProducer generatedResourceBuildItemBuildProducer) { - + log.info("Processing {} datasource annotations into build items", datasources.size()); List decls = new ArrayList<>(); List namedDatasources = new ArrayList<>(); for (var ds : datasources) { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java index dbf37d92d4..64214c8400 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java @@ -11,6 +11,8 @@ 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; @@ -27,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); @Override public String providerId() { @@ -40,6 +43,7 @@ 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; } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java index bd2c31c4c9..8de99b8c76 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java @@ -16,7 +16,8 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.ParameterizedType; -import org.jboss.logging.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.tomlj.Toml; import org.tomlj.TomlParseResult; @@ -48,7 +49,7 @@ public class ModuleProcessor { - private static final Logger log = Logger.getLogger(ModuleProcessor.class); + private static final Logger log = LoggerFactory.getLogger(ModuleProcessor.class); private static final String FEATURE = "ftl-java-runtime"; @@ -89,7 +90,7 @@ ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem if (value != null) { return new ModuleNameBuildItem(value); } else { - log.errorf("module name not found in %s", toml); + log.error("module name not found in {}", toml); } } if (source.getParent() == null) { @@ -113,6 +114,7 @@ public void generateSchema(CombinedIndexBuildItem index, VerbClientBuildItem verbClientBuildItem, List typeAliasBuildItems, List schemaContributorBuildItems) throws Exception { + log.info("Generating module '{}' schema from build items", moduleNameBuildItem.getModuleName()); String moduleName = moduleNameBuildItem.getModuleName(); Map> comments = readComments(); Map existingRefs = new HashMap<>(); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java index 18a61b47bf..3e66badb26 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java @@ -1,12 +1,15 @@ package xyz.block.ftl.deployment; +import java.util.Collection; import java.util.HashMap; import java.util.Map; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; -import org.jboss.logging.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -22,13 +25,16 @@ public class SubscriptionProcessor { - private static final Logger log = Logger.getLogger(SubscriptionProcessor.class); + private static final Logger log = LoggerFactory.getLogger(SubscriptionProcessor.class); @BuildStep SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildItem combinedIndexBuildItem, ModuleNameBuildItem moduleNameBuildItem) { + Collection subscriptionAnnotations = combinedIndexBuildItem.getComputingIndex() + .getAnnotations(Subscription.class); + log.info("Processing {} subscription annotations into build items", subscriptionAnnotations.size()); Map annotations = new HashMap<>(); - for (var subscriptions : combinedIndexBuildItem.getComputingIndex().getAnnotations(Subscription.class)) { + for (var subscriptions : subscriptionAnnotations) { if (subscriptions.target().kind() != AnnotationTarget.Kind.CLASS) { continue; } @@ -59,7 +65,7 @@ public void registerSubscriptions(CombinedIndexBuildItem index, for (var metaSub : subscriptionMetaAnnotationsBuildItem.getAnnotations().entrySet()) { for (var subscription : index.getIndex().getAnnotations(metaSub.getKey())) { if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { - log.warnf("Subscription annotation on non-method target: %s", subscription.target()); + log.warn("Subscription annotation on non-method target: {}", subscription.target()); continue; } var method = subscription.target().asMethod(); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java index 5f70c350e9..31d9cbe2d3 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java @@ -8,6 +8,8 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; @@ -25,10 +27,12 @@ public class TopicsProcessor { public static final DotName TOPIC = DotName.createSimple(Topic.class); + private static final Logger log = LoggerFactory.getLogger(TopicsProcessor.class); @BuildStep TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer generatedTopicProducer) { var topicDefinitions = index.getComputingIndex().getAnnotations(TopicDefinition.class); + log.info("Processing {} topic definition annotations into build items", topicDefinitions.size()); Map topics = new HashMap<>(); Set names = new HashSet<>(); for (var topicDefinition : topicDefinitions) { @@ -82,6 +86,7 @@ TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer() { @Override diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java index 299edb5b56..34ddc379b4 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java @@ -1,7 +1,12 @@ package xyz.block.ftl.deployment; +import java.util.Collection; + +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.Type; import org.jboss.jandex.TypeVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -10,13 +15,17 @@ public class TypeAliasProcessor { + private static final Logger log = LoggerFactory.getLogger(TypeAliasProcessor.class); + @BuildStep public void processTypeAlias(CombinedIndexBuildItem index, BuildProducer schemaContributorBuildItemBuildProducer, BuildProducer additionalBeanBuildItem, BuildProducer typeAliasBuildItemBuildProducer) { + Collection typeAliasAnnotations = index.getIndex().getAnnotations(FTLDotNames.TYPE_ALIAS); + log.info("Processing {} type alias annotations into build items", typeAliasAnnotations.size()); var beans = new AdditionalBeanBuildItem.Builder().setUnremovable(); - for (var mapper : index.getIndex().getAnnotations(FTLDotNames.TYPE_ALIAS)) { + for (var mapper : typeAliasAnnotations) { boolean exported = mapper.target().hasAnnotation(FTLDotNames.EXPORT); // This may or may not be the actual mapper, it may be a subclass From 4bd062d1efb4c52c1b2aea97e6ef181d90e01e80 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Mon, 30 Sep 2024 19:47:07 +1000 Subject: [PATCH 10/34] Quarkus build processors log their task count --- .../xyz/block/ftl/deployment/VerbProcessor.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java index e746bd0e99..7a3f73ffc1 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java @@ -1,14 +1,18 @@ package xyz.block.ftl.deployment; +import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import jakarta.inject.Singleton; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; import org.jboss.jandex.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; @@ -38,6 +42,7 @@ public class VerbProcessor { public static final DotName VERB_CLIENT_SOURCE = DotName.createSimple(VerbClientSource.class); public static final DotName VERB_CLIENT_EMPTY = DotName.createSimple(VerbClientEmpty.class); public static final String TEST_ANNOTATION = "xyz.block.ftl.java.test.FTLManaged"; + private static final Logger log = LoggerFactory.getLogger(VerbProcessor.class); @BuildStep VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProducer generatedClients, @@ -45,6 +50,7 @@ VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProduce ModuleNameBuildItem moduleNameBuildItem, LaunchModeBuildItem launchModeBuildItem) { var clientDefinitions = index.getComputingIndex().getAnnotations(VerbClientDefinition.class); + log.info("Processing {} verb clients into build items", clientDefinitions.size()); Map clients = new HashMap<>(); for (var clientDefinition : clientDefinitions) { var iface = clientDefinition.target().asClass(); @@ -226,8 +232,10 @@ VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProduce public void verbsAndCron(CombinedIndexBuildItem index, BuildProducer additionalBeanBuildItem, BuildProducer schemaContributorBuildItemBuildProducer) { + Collection verbAnnotations = index.getIndex().getAnnotations(FTLDotNames.VERB); + log.info("Processing {} verb annotations into schema build items", verbAnnotations.size()); var beans = AdditionalBeanBuildItem.builder().setUnremovable(); - for (var verb : index.getIndex().getAnnotations(FTLDotNames.VERB)) { + for (var verb : verbAnnotations) { boolean exported = verb.target().hasAnnotation(FTLDotNames.EXPORT); var method = verb.target().asMethod(); String className = method.declaringClass().name().toString(); @@ -235,7 +243,10 @@ public void verbsAndCron(CombinedIndexBuildItem index, schemaContributorBuildItemBuildProducer.produce(new SchemaContributorBuildItem(moduleBuilder -> moduleBuilder .registerVerbMethod(method, className, exported, ModuleBuilder.BodyType.ALLOWED, null))); } - for (var cron : index.getIndex().getAnnotations(FTLDotNames.CRON)) { + + Collection cronAnnotations = index.getIndex().getAnnotations(FTLDotNames.CRON); + log.info("Processing {} cron job annotations into schema build items", cronAnnotations.size()); + for (var cron : cronAnnotations) { var method = cron.target().asMethod(); String className = method.declaringClass().name().toString(); beans.addBeanClass(className); From 5066685a6e528cefe2b16617af367da887f9f8ee Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Mon, 30 Sep 2024 19:51:04 +1000 Subject: [PATCH 11/34] Extract schema for value enums --- .../block/ftl/deployment/EnumProcessor.java | 88 +++++++++++++++++++ .../xyz/block/ftl/deployment/FTLDotNames.java | 2 + 2 files changed, 90 insertions(+) create mode 100644 jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java new file mode 100644 index 0000000000..e027b2ad04 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -0,0 +1,88 @@ +package xyz.block.ftl.deployment; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +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.builditem.CombinedIndexBuildItem; +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.Value; + +public class EnumProcessor { + + private static final Logger log = LoggerFactory.getLogger(EnumProcessor.class); + + @BuildStep + SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { + var enumAnnotations = index.getIndex().getAnnotations(FTLDotNames.ENUM); + log.info("Processing {} enum annotations into build items", enumAnnotations.size()); + List decls = new ArrayList<>(); + try { + for (var enumAnnotation : enumAnnotations) { + boolean exported = enumAnnotation.target().hasAnnotation(FTLDotNames.EXPORT); + ClassInfo enumClassInfo = enumAnnotation.target().asClass(); + Enum.Builder enumBuilder = Enum.newBuilder() + .setName(enumClassInfo.simpleName()) + .setExport(exported); + if (enumClassInfo.isEnum()) { + // Value enums must have a type + Type type = enumClassInfo.field("value").type(); + xyz.block.ftl.v1.schema.Type.Builder typeBuilder = xyz.block.ftl.v1.schema.Type.newBuilder(); + if (type == PrimitiveType.LONG) { + typeBuilder.setInt(Int.newBuilder().build()).build(); + } else { + typeBuilder.setString(xyz.block.ftl.v1.schema.String.newBuilder().build()); + } + enumBuilder.setType(typeBuilder.build()); + + Class enumClass = Class.forName(enumClassInfo.name().toString(), false, + Thread.currentThread().getContextClassLoader()); + for (var constant : enumClass.getEnumConstants()) { + Field value = constant.getClass().getDeclaredField("value"); + value.setAccessible(true); + Value.Builder valueBuilder = Value.newBuilder(); + if (type == PrimitiveType.LONG) { + long aLong = value.getLong(constant); + valueBuilder.setIntValue(IntValue.newBuilder().setValue(aLong).build()); + } else if (type.name().equals(DotName.STRING_NAME)) { + String aString = (String) value.get(constant); + valueBuilder.setStringValue(StringValue.newBuilder().setValue(aString).build()); + } else { + throw new RuntimeException("Unsupported enum value type: " + value.getType()); + } + EnumVariant variant = EnumVariant.newBuilder() + .setName(constant.toString()) + .setValue(valueBuilder) + .build(); + enumBuilder.addVariants(variant); + } + // TODO move outside if + decls.add(Decl.newBuilder().setEnum(enumBuilder).build()); + } else { + // Type enums + // TODO + } + + } + return new SchemaContributorBuildItem(decls); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java index 42de5a1d6e..84c6561426 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java @@ -4,6 +4,7 @@ import xyz.block.ftl.Config; import xyz.block.ftl.Cron; +import xyz.block.ftl.Enum; import xyz.block.ftl.Export; import xyz.block.ftl.LeaseClient; import xyz.block.ftl.Secret; @@ -21,6 +22,7 @@ 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 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); From 9fc18728bd2ac7477102c091c84795b30dd6d3af Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Tue, 1 Oct 2024 10:48:31 +1000 Subject: [PATCH 12/34] Validate enums field and type --- .../block/ftl/deployment/EnumProcessor.java | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index e027b2ad04..c1f5b9def5 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -1,14 +1,18 @@ 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 java.lang.reflect.Field; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.Set; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +37,7 @@ SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { log.info("Processing {} enum annotations into build items", enumAnnotations.size()); List decls = new ArrayList<>(); try { + // TODO how do we exclude @Enum annotations from generated verb clients? for (var enumAnnotation : enumAnnotations) { boolean exported = enumAnnotation.target().hasAnnotation(FTLDotNames.EXPORT); ClassInfo enumClassInfo = enumAnnotation.target().asClass(); @@ -41,12 +46,19 @@ SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { .setExport(exported); if (enumClassInfo.isEnum()) { // Value enums must have a type - Type type = enumClassInfo.field("value").type(); + FieldInfo valueField = enumClassInfo.field("value"); + if (valueField == null) { + throw new RuntimeException("Enum must have a 'value' field: " + enumClassInfo.name()); + } + Type type = valueField.type(); xyz.block.ftl.v1.schema.Type.Builder typeBuilder = xyz.block.ftl.v1.schema.Type.newBuilder(); - if (type == PrimitiveType.LONG) { + if (isInt(type)) { typeBuilder.setInt(Int.newBuilder().build()).build(); - } else { + } 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: " + enumClassInfo.name()); } enumBuilder.setType(typeBuilder.build()); @@ -56,14 +68,12 @@ SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { Field value = constant.getClass().getDeclaredField("value"); value.setAccessible(true); Value.Builder valueBuilder = Value.newBuilder(); - if (type == PrimitiveType.LONG) { + if (isInt(type)) { long aLong = value.getLong(constant); valueBuilder.setIntValue(IntValue.newBuilder().setValue(aLong).build()); - } else if (type.name().equals(DotName.STRING_NAME)) { + } else { String aString = (String) value.get(constant); valueBuilder.setStringValue(StringValue.newBuilder().setValue(aString).build()); - } else { - throw new RuntimeException("Unsupported enum value type: " + value.getType()); } EnumVariant variant = EnumVariant.newBuilder() .setName(constant.toString()) @@ -85,4 +95,11 @@ SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { } } + private boolean isInt(Type type) { + if (type.kind() != Type.Kind.PRIMITIVE) { + return false; + } + return Set.of(INT, LONG, BYTE, SHORT).contains(type.asPrimitiveType().primitive()); + } + } From 085784d5ed4442daf0db7a4b76499f7b36e5ef31 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Tue, 1 Oct 2024 17:30:49 +1000 Subject: [PATCH 13/34] Properly handle enums. Validate schema name clashes --- .../block/ftl/deployment/EnumProcessor.java | 4 + .../xyz/block/ftl/deployment/FTLDotNames.java | 2 + .../block/ftl/deployment/ModuleBuilder.java | 177 ++++++++++++------ .../block/ftl/deployment/ModuleProcessor.java | 23 +-- jvm-runtime/testdata/go/gomodule/server.go | 6 + .../java/xyz/block/ftl/enums/ColorInt.java | 20 ++ .../main/java/xyz/block/ftl/enums/Shape.java | 20 ++ .../xyz/block/ftl/{test => enums}/Verbs.java | 18 +- 8 files changed, 187 insertions(+), 83 deletions(-) create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorInt.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Shape.java rename jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/{test => enums}/Verbs.java (62%) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index c1f5b9def5..9e07d754ba 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -4,6 +4,7 @@ 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.GENERATED_REF; import java.lang.reflect.Field; import java.util.ArrayList; @@ -41,6 +42,9 @@ SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { for (var enumAnnotation : enumAnnotations) { boolean exported = enumAnnotation.target().hasAnnotation(FTLDotNames.EXPORT); ClassInfo enumClassInfo = enumAnnotation.target().asClass(); + if (enumClassInfo.hasDeclaredAnnotation(GENERATED_REF)) { + continue; + } Enum.Builder enumBuilder = Enum.newBuilder() .setName(enumClassInfo.simpleName()) .setExport(exported); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java index 84c6561426..23efa4c783 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java @@ -6,6 +6,7 @@ import xyz.block.ftl.Cron; import xyz.block.ftl.Enum; 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; @@ -29,4 +30,5 @@ private FTLDotNames() { 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); } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index 607a7dcf3c..d29481bc6c 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -1,5 +1,9 @@ package xyz.block.ftl.deployment; +import static xyz.block.ftl.deployment.FTLDotNames.ENUM; +import static xyz.block.ftl.deployment.FTLDotNames.EXPORT; +import static xyz.block.ftl.deployment.FTLDotNames.GENERATED_REF; + import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Modifier; @@ -33,8 +37,6 @@ import io.quarkus.arc.processor.DotNames; import xyz.block.ftl.Config; -import xyz.block.ftl.Export; -import xyz.block.ftl.GeneratedRef; import xyz.block.ftl.LeaseClient; import xyz.block.ftl.Secret; import xyz.block.ftl.VerbName; @@ -74,13 +76,12 @@ public class ModuleBuilder { public static final DotName NOT_NULL = DotName.createSimple(NotNull.class); public static final DotName JSON_NODE = DotName.createSimple(JsonNode.class.getName()); public static final DotName OFFSET_DATE_TIME = DotName.createSimple(OffsetDateTime.class.getName()); - public static final DotName GENERATED_REF = DotName.createSimple(GeneratedRef.class); - public static final DotName EXPORT = DotName.createSimple(Export.class); + private static final Pattern NAME_PATTERN = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$"); private final IndexView index; - private final Module.Builder moduleBuilder; - private final Map dataElements; + private final Module.Builder protoModuleBuilder; + private final Map decls = new HashMap<>(); private final String moduleName; private final Set knownSecrets = new HashSet<>(); private final Set knownConfig = new HashSet<>(); @@ -92,17 +93,16 @@ public class ModuleBuilder { public ModuleBuilder(IndexView index, String moduleName, Map knownTopics, Map verbClients, FTLRecorder recorder, - Map> comments, Map typeAliases) { + Map> comments) { this.index = index; this.moduleName = moduleName; - this.moduleBuilder = Module.newBuilder() + this.protoModuleBuilder = Module.newBuilder() .setName(moduleName) .setBuiltin(false); this.knownTopics = knownTopics; this.verbClients = verbClients; this.recorder = recorder; this.comments = comments; - this.dataElements = new HashMap<>(typeAliases); } public static @NotNull String methodToName(MethodInfo method) { @@ -321,10 +321,11 @@ public Type buildType(org.jboss.jandex.Type type, boolean export) { .setType(primitive)) .build(); } - if (info != null && info.hasDeclaredAnnotation(GENERATED_REF)) { + if (info.hasDeclaredAnnotation(GENERATED_REF)) { var ref = info.declaredAnnotation(GENERATED_REF); return Type.newBuilder() - .setRef(Ref.newBuilder().setName(ref.value("name").asString()) + .setRef(Ref.newBuilder() + .setName(ref.value("name").asString()) .setModule(ref.value("module").asString())) .build(); } @@ -343,36 +344,36 @@ public Type buildType(org.jboss.jandex.Type type, boolean export) { if (clazz.name().equals(ZONED_DATE_TIME)) { return Type.newBuilder().setTime(Time.newBuilder().build()).build(); } - var existing = dataElements.get(new TypeKey(clazz.name().toString(), List.of())); - if (existing != null) { - if (existing.exported() || !export || !existing.ref().getModule().equals(moduleName)) { - return Type.newBuilder().setRef(existing.ref()).build(); - } - //bit of an edge case, we have an existing non-exported object that we need to export - for (var i = 0; i < moduleBuilder.getDeclsCount(); ++i) { - var decl = moduleBuilder.getDecls(i); - if (!decl.hasData()) { - continue; - } - if (decl.getData().getName().equals(existing.ref().getName())) { - moduleBuilder.setDecls(i, - decl.toBuilder().setData(decl.getData().toBuilder().setExport(true)).build()); - break; - } + + var ref = Type.newBuilder().setRef(Ref.newBuilder() + .setName(clazz.name().local()) + .setModule(moduleName) + .build()) + .build(); + + if (info.isEnum() || info.hasAnnotation(ENUM)) { + // We set only the name and export here. EnumProcessor will fill in the rest + xyz.block.ftl.v1.schema.Enum ennum = xyz.block.ftl.v1.schema.Enum.newBuilder() + .setName(clazz.name().local()) + .setExport(type.hasAnnotation(EXPORT) || export) + .build(); + addDecls(Decl.newBuilder().setEnum(ennum).build()); + return ref; + } else { + // If we've processed this data already, skip early + if (updateData(clazz.name().local(), type.hasAnnotation(EXPORT) || export)) { + return ref; } - return Type.newBuilder().setRef(existing.ref()).build(); + + Data.Builder data = Data.newBuilder(); + data.setName(clazz.name().local()); + data.setExport(type.hasAnnotation(EXPORT) || export); + Optional.ofNullable(comments.get(CommentKey.ofData(clazz.name().local()))) + .ifPresent(data::addAllComments); + buildDataElement(data, clazz.name()); + addDecls(Decl.newBuilder().setData(data).build()); + return ref; } - Data.Builder data = Data.newBuilder(); - data.setName(clazz.name().local()); - data.setExport(type.hasAnnotation(EXPORT) || export); - Optional.ofNullable(comments.get(CommentKey.ofData(clazz.name().local()))) - .ifPresent(data::addAllComments); - buildDataElement(data, clazz.name()); - moduleBuilder.addDecls(Decl.newBuilder().setData(data).build()); - Ref ref = Ref.newBuilder().setName(data.getName()).setModule(moduleName).build(); - dataElements.put(new TypeKey(clazz.name().toString(), List.of()), - new ExistingRef(ref, export || data.getExport())); - return Type.newBuilder().setRef(ref).build(); } case PARAMETERIZED_TYPE -> { var paramType = type.asParameterizedType(); @@ -450,34 +451,42 @@ private void buildDataElement(Data.Builder data, DotName className) { } public ModuleBuilder addDecls(Decl decl) { - if (decl.hasDatabase()) { - validateName(decl.getDatabase().getPos(), decl.getDatabase().getName()); - } else if (decl.hasData()) { - validateName(decl.getData().getPos(), decl.getData().getName()); - } else if (decl.hasConfig()) { - validateName(decl.getConfig().getPos(), decl.getConfig().getName()); + if (decl.hasData()) { + Data data = decl.getData(); + if (updateData(data.getName(), data.getExport())) { + return this; + } + addDecl(decl, data.getPos(), data.getName()); } else if (decl.hasEnum()) { - validateName(decl.getEnum().getPos(), decl.getEnum().getName()); + xyz.block.ftl.v1.schema.Enum enuum = decl.getEnum(); + if (updateEnum(enuum.getName(), decl)) { + return this; + } + addDecl(decl, enuum.getPos(), enuum.getName()); + } else if (decl.hasDatabase()) { + addDecl(decl, decl.getDatabase().getPos(), decl.getDatabase().getName()); + } else if (decl.hasConfig()) { + addDecl(decl, decl.getConfig().getPos(), decl.getConfig().getName()); } else if (decl.hasSecret()) { - validateName(decl.getSecret().getPos(), decl.getSecret().getName()); + addDecl(decl, decl.getSecret().getPos(), decl.getSecret().getName()); } else if (decl.hasVerb()) { - validateName(decl.getVerb().getPos(), decl.getVerb().getName()); + addDecl(decl, decl.getVerb().getPos(), decl.getVerb().getName()); } else if (decl.hasTypeAlias()) { - validateName(decl.getTypeAlias().getPos(), decl.getTypeAlias().getName()); + addDecl(decl, decl.getTypeAlias().getPos(), decl.getTypeAlias().getName()); } else if (decl.hasTopic()) { - validateName(decl.getTopic().getPos(), decl.getTopic().getName()); + addDecl(decl, decl.getTopic().getPos(), decl.getTopic().getName()); } else if (decl.hasFsm()) { - validateName(decl.getFsm().getPos(), decl.getFsm().getName()); + addDecl(decl, decl.getFsm().getPos(), decl.getFsm().getName()); } else if (decl.hasSubscription()) { - validateName(decl.getSubscription().getPos(), decl.getSubscription().getName()); - } else if (decl.hasTypeAlias()) { - validateName(decl.getTypeAlias().getPos(), decl.getTypeAlias().getName()); + addDecl(decl, decl.getSubscription().getPos(), decl.getSubscription().getName()); } - moduleBuilder.addDecls(decl); + return this; } public void writeTo(OutputStream out) throws IOException { + decls.values().stream().forEachOrdered(protoModuleBuilder::addDecls); + if (!validationFailures.isEmpty()) { StringBuilder sb = new StringBuilder(); for (var failure : validationFailures) { @@ -486,12 +495,11 @@ public void writeTo(OutputStream out) throws IOException { } throw new RuntimeException(sb.toString()); } - moduleBuilder.build().writeTo(out); + protoModuleBuilder.build().writeTo(out); } public void registerTypeAlias(String name, org.jboss.jandex.Type finalT, org.jboss.jandex.Type finalS, boolean exported) { - validateName(finalT.name().toString(), name); - moduleBuilder.addDecls(Decl.newBuilder() + addDecls(Decl.newBuilder() .setTypeAlias(TypeAlias.newBuilder().setType(buildType(finalS, exported)).setName(name).addMetadata(Metadata .newBuilder() .setTypeMap(MetadataTypeMap.newBuilder().setRuntime("java").setNativeName(finalT.toString()).build()) @@ -499,6 +507,59 @@ public void registerTypeAlias(String name, org.jboss.jandex.Type finalT, org.jbo .build()); } + private void addDecl(Decl decl, Position pos, String name) { + validateName(pos, name); + if (decls.containsKey(name)) { + duplicateNameValidationError(name, pos); + } + decls.put(name, decl); + } + + /** + * Check if an enum with the given name already exists in the module. If it does, merge fields from both into one + */ + private boolean updateEnum(String name, Decl decl) { + if (decls.containsKey(name)) { + var existing = decls.get(name); + if (!existing.hasEnum()) { + duplicateNameValidationError(name, decl.getEnum().getPos()); + } + var moreComplete = decl.getEnum().getVariantsCount() > 0 ? decl : existing; + var merged = existing.getEnum().toBuilder() + .setName(moreComplete.getEnum().getName()) + .setExport(decl.getEnum().getExport() || existing.getEnum().getExport()) + .addAllVariants(moreComplete.getEnum().getVariantsList()) + .addAllComments(moreComplete.getEnum().getCommentsList()) + .setType(moreComplete.getEnum().getType()).build(); + decls.put(name, Decl.newBuilder().setEnum(merged).build()); + return true; + } + return false; + } + + /** + * Check if a data with the given name already exists in the module. If it does, update its export field to + * match export, and return true + */ + private boolean updateData(String name, boolean export) { + if (decls.containsKey(name)) { + var existing = decls.get(name); + if (!existing.hasData()) { + duplicateNameValidationError(name, existing.getEnum().getPos()); + } + var merged = existing.getData().toBuilder().setExport(export).build(); + decls.put(name, Decl.newBuilder().setData(merged).build()); + return true; + } + return false; + } + + private void duplicateNameValidationError(String name, Position pos) { + validationFailures.add(new ValidationFailure(name, String.format( + "schema declaration with name \"%s\" already exists for module \"%s\"; previously declared at \"%s\"", + name, moduleName, pos.getFilename() + ":" + pos.getLine()))); + } + record ExistingRef(Ref ref, boolean exported) { } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java index 8de99b8c76..08348fe9fc 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java @@ -15,7 +15,6 @@ import java.util.stream.Collectors; import org.jboss.jandex.DotName; -import org.jboss.jandex.ParameterizedType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tomlj.Toml; @@ -45,7 +44,6 @@ import xyz.block.ftl.runtime.VerbRegistry; import xyz.block.ftl.runtime.config.FTLConfigSourceFactoryBuilder; import xyz.block.ftl.runtime.http.FTLHttpHandler; -import xyz.block.ftl.v1.schema.Ref; public class ModuleProcessor { @@ -112,30 +110,13 @@ public void generateSchema(CombinedIndexBuildItem index, ModuleNameBuildItem moduleNameBuildItem, TopicsBuildItem topicsBuildItem, VerbClientBuildItem verbClientBuildItem, - List typeAliasBuildItems, List schemaContributorBuildItems) throws Exception { - log.info("Generating module '{}' schema from build items", moduleNameBuildItem.getModuleName()); String moduleName = moduleNameBuildItem.getModuleName(); + log.info("Generating module '{}' schema from build items", moduleName); Map> comments = readComments(); - Map existingRefs = new HashMap<>(); - for (var i : typeAliasBuildItems) { - String mn; - if (i.getModule().isEmpty()) { - mn = moduleNameBuildItem.getModuleName(); - } else { - mn = i.getModule(); - } - if (i.getLocalType() instanceof ParameterizedType) { - //TODO: we can't handle this yet - // existingRefs.put(new TypeKey(i.getLocalType().name().toString(), i.getLocalType().asParameterizedType().arguments().stream().map(i.)), new ModuleBuilder.ExistingRef(Ref.newBuilder().setModule(moduleName).setName(i.getName()).build(), i.isExported())); - } else { - existingRefs.put(new TypeKey(i.getLocalType().name().toString(), List.of()), new ModuleBuilder.ExistingRef( - Ref.newBuilder().setModule(mn).setName(i.getName()).build(), i.isExported())); - } - } ModuleBuilder moduleBuilder = new ModuleBuilder(index.getComputingIndex(), moduleName, topicsBuildItem.getTopics(), - verbClientBuildItem.getVerbClients(), recorder, comments, existingRefs); + verbClientBuildItem.getVerbClients(), recorder, comments); for (var i : schemaContributorBuildItems) { i.getSchemaContributor().accept(moduleBuilder); diff --git a/jvm-runtime/testdata/go/gomodule/server.go b/jvm-runtime/testdata/go/gomodule/server.go index e002840602..e27996f848 100644 --- a/jvm-runtime/testdata/go/gomodule/server.go +++ b/jvm-runtime/testdata/go/gomodule/server.go @@ -7,6 +7,7 @@ import ( "github.com/tbd54566975/web5-go/dids/did" + "ftl/javaserver" "github.com/TBD54566975/ftl/go-runtime/ftl" ) @@ -281,3 +282,8 @@ func GetAnimal(ctx context.Context) (AnimalWrapper, error) { //func MixedEnumVerb(ctx context.Context, val Mixed) (Mixed, error) { // return val, nil //} + +func callJavaServer(ctx context.Context, req javaserver.ColorInt, getValueEnum javaserver.ValueEnumVerbClient) (javaserver.ColorInt, error) { + color, _ := getValueEnum(ctx, req) + return color, nil +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorInt.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorInt.java new file mode 100644 index 0000000000..fb8d3e79fc --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorInt.java @@ -0,0 +1,20 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.Enum; + +@Enum +public enum ColorInt { + RED(0), + GREEN(1), + BLUE(2); + + private final int value; + + ColorInt(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Shape.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Shape.java new file mode 100644 index 0000000000..3c45a283f1 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Shape.java @@ -0,0 +1,20 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.Enum; + +@Enum +public enum Shape { + CIRCLE("circle"), + SQUARE("square"), + TRIANGLE("triangle"); + + private final String value; + + Shape(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/test/Verbs.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java similarity index 62% rename from jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/test/Verbs.java rename to jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java index 1880cfdb52..3873dbfa5f 100644 --- a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/test/Verbs.java +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java @@ -1,12 +1,12 @@ -package xyz.block.ftl.test; +package xyz.block.ftl.enums; + +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; + import xyz.block.ftl.Export; import xyz.block.ftl.Verb; -import java.util.Map; - public class Verbs { @Export @@ -21,5 +21,15 @@ public Object anyOutput(String name) { return Map.of("name", name); } + @Export + @Verb + public ColorInt valueEnumVerb(ColorInt color) { + return color; + } + @Export + @Verb + public Shape stringEnumVerb(Shape shape) { + return shape; + } } From 15fc6b7be1a79112804862eaa19a867e2a48b66e Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 2 Oct 2024 07:50:41 +1000 Subject: [PATCH 14/34] Clean up logs from processors --- .../java/xyz/block/ftl/deployment/DatasourceProcessor.java | 2 +- .../src/main/java/xyz/block/ftl/deployment/EnumProcessor.java | 2 +- .../src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java | 4 ++++ .../main/java/xyz/block/ftl/deployment/ModuleProcessor.java | 2 +- .../java/xyz/block/ftl/deployment/SubscriptionProcessor.java | 2 +- .../main/java/xyz/block/ftl/deployment/TopicsProcessor.java | 3 +-- .../java/xyz/block/ftl/deployment/TypeAliasProcessor.java | 2 +- .../src/main/java/xyz/block/ftl/deployment/VerbProcessor.java | 4 ++-- 8 files changed, 12 insertions(+), 9 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java index 7d9db125f1..c2dcd99ffe 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java @@ -26,7 +26,7 @@ public SchemaContributorBuildItem registerDatasources( List datasources, BuildProducer systemPropProducer, BuildProducer generatedResourceBuildItemBuildProducer) { - log.info("Processing {} datasource annotations into build items", datasources.size()); + log.info("Processing {} datasource annotations into decls", datasources.size()); List decls = new ArrayList<>(); List namedDatasources = new ArrayList<>(); for (var ds : datasources) { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index 9e07d754ba..e5d04dee14 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -35,7 +35,7 @@ public class EnumProcessor { @BuildStep SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { var enumAnnotations = index.getIndex().getAnnotations(FTLDotNames.ENUM); - log.info("Processing {} enum annotations into build items", enumAnnotations.size()); + log.info("Processing {} enum annotations into decls", enumAnnotations.size()); List decls = new ArrayList<>(); try { // TODO how do we exclude @Enum annotations from generated verb clients? diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index d29481bc6c..78abc19857 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -484,6 +484,10 @@ public ModuleBuilder addDecls(Decl decl) { return this; } + public int getDeclsCount() { + return decls.size(); + } + public void writeTo(OutputStream out) throws IOException { decls.values().stream().forEachOrdered(protoModuleBuilder::addDecls); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java index 08348fe9fc..930f00e31a 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java @@ -112,7 +112,6 @@ public void generateSchema(CombinedIndexBuildItem index, VerbClientBuildItem verbClientBuildItem, List schemaContributorBuildItems) throws Exception { String moduleName = moduleNameBuildItem.getModuleName(); - log.info("Generating module '{}' schema from build items", moduleName); Map> comments = readComments(); ModuleBuilder moduleBuilder = new ModuleBuilder(index.getComputingIndex(), moduleName, topicsBuildItem.getTopics(), @@ -122,6 +121,7 @@ public void generateSchema(CombinedIndexBuildItem index, i.getSchemaContributor().accept(moduleBuilder); } + log.info("Generating module '{}' schema from {} decls", moduleName, moduleBuilder.getDeclsCount()); Path output = outputTargetBuildItem.getOutputDirectory().resolve(SCHEMA_OUT); try (var out = Files.newOutputStream(output)) { moduleBuilder.writeTo(out); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java index 3e66badb26..5505b5d47e 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java @@ -32,7 +32,7 @@ SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildI ModuleNameBuildItem moduleNameBuildItem) { Collection subscriptionAnnotations = combinedIndexBuildItem.getComputingIndex() .getAnnotations(Subscription.class); - log.info("Processing {} subscription annotations into build items", subscriptionAnnotations.size()); + log.info("Processing {} subscription annotations into decls", subscriptionAnnotations.size()); Map annotations = new HashMap<>(); for (var subscriptions : subscriptionAnnotations) { if (subscriptions.target().kind() != AnnotationTarget.Kind.CLASS) { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java index 31d9cbe2d3..588a6bd83d 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java @@ -32,7 +32,7 @@ public class TopicsProcessor { @BuildStep TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer generatedTopicProducer) { var topicDefinitions = index.getComputingIndex().getAnnotations(TopicDefinition.class); - log.info("Processing {} topic definition annotations into build items", topicDefinitions.size()); + log.info("Processing {} topic definition annotations into decls", topicDefinitions.size()); Map topics = new HashMap<>(); Set names = new HashSet<>(); for (var topicDefinition : topicDefinitions) { @@ -86,7 +86,6 @@ TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer() { @Override diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java index 34ddc379b4..545b2fd12b 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java @@ -23,7 +23,7 @@ public void processTypeAlias(CombinedIndexBuildItem index, BuildProducer additionalBeanBuildItem, BuildProducer typeAliasBuildItemBuildProducer) { Collection typeAliasAnnotations = index.getIndex().getAnnotations(FTLDotNames.TYPE_ALIAS); - log.info("Processing {} type alias annotations into build items", typeAliasAnnotations.size()); + log.info("Processing {} type alias annotations into decls", typeAliasAnnotations.size()); var beans = new AdditionalBeanBuildItem.Builder().setUnremovable(); for (var mapper : typeAliasAnnotations) { boolean exported = mapper.target().hasAnnotation(FTLDotNames.EXPORT); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java index 7a3f73ffc1..a8f32d5ebb 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java @@ -233,7 +233,7 @@ public void verbsAndCron(CombinedIndexBuildItem index, BuildProducer additionalBeanBuildItem, BuildProducer schemaContributorBuildItemBuildProducer) { Collection verbAnnotations = index.getIndex().getAnnotations(FTLDotNames.VERB); - log.info("Processing {} verb annotations into schema build items", verbAnnotations.size()); + log.info("Processing {} verb annotations into decls", verbAnnotations.size()); var beans = AdditionalBeanBuildItem.builder().setUnremovable(); for (var verb : verbAnnotations) { boolean exported = verb.target().hasAnnotation(FTLDotNames.EXPORT); @@ -245,7 +245,7 @@ public void verbsAndCron(CombinedIndexBuildItem index, } Collection cronAnnotations = index.getIndex().getAnnotations(FTLDotNames.CRON); - log.info("Processing {} cron job annotations into schema build items", cronAnnotations.size()); + log.info("Processing {} cron job annotations into decls", cronAnnotations.size()); for (var cron : cronAnnotations) { var method = cron.target().asMethod(); String className = method.declaringClass().name().toString(); From 8b1f40b3c12b2f5e1d94b421581029c17b9fb8b7 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 2 Oct 2024 12:39:31 +1000 Subject: [PATCH 15/34] Extract schema from type enums --- .../block/ftl/deployment/EnumProcessor.java | 136 +++++++++++------- .../block/ftl/deployment/ModuleBuilder.java | 22 ++- .../main/java/xyz/block/ftl/enums/Animal.java | 14 ++ .../main/java/xyz/block/ftl/enums/Cat.java | 22 +++ .../main/java/xyz/block/ftl/enums/Dog.java | 22 +++ .../main/java/xyz/block/ftl/enums/Verbs.java | 6 + 6 files changed, 155 insertions(+), 67 deletions(-) create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Animal.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index e5d04dee14..1b06e67f3f 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -10,8 +10,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.Type; @@ -26,6 +28,7 @@ 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 { @@ -36,67 +39,94 @@ public class EnumProcessor { SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { var enumAnnotations = index.getIndex().getAnnotations(FTLDotNames.ENUM); log.info("Processing {} enum annotations into decls", enumAnnotations.size()); - List decls = new ArrayList<>(); - try { - // TODO how do we exclude @Enum annotations from generated verb clients? - for (var enumAnnotation : enumAnnotations) { - boolean exported = enumAnnotation.target().hasAnnotation(FTLDotNames.EXPORT); - ClassInfo enumClassInfo = enumAnnotation.target().asClass(); - if (enumClassInfo.hasDeclaredAnnotation(GENERATED_REF)) { - continue; - } - Enum.Builder enumBuilder = Enum.newBuilder() - .setName(enumClassInfo.simpleName()) - .setExport(exported); - if (enumClassInfo.isEnum()) { - // Value enums must have a type - FieldInfo valueField = enumClassInfo.field("value"); - if (valueField == null) { - throw new RuntimeException("Enum must have a 'value' field: " + enumClassInfo.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: " + enumClassInfo.name()); - } - enumBuilder.setType(typeBuilder.build()); - Class enumClass = Class.forName(enumClassInfo.name().toString(), false, - Thread.currentThread().getContextClassLoader()); - for (var constant : enumClass.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()); + return new SchemaContributorBuildItem(new Consumer() { + @Override + public void accept(ModuleBuilder moduleBuilder) { + List decls = new ArrayList<>(); + try { + for (var enumAnnotation : enumAnnotations) { + boolean exported = enumAnnotation.target().hasAnnotation(FTLDotNames.EXPORT); + ClassInfo enumClassInfo = enumAnnotation.target().asClass(); + if (enumClassInfo.hasDeclaredAnnotation(GENERATED_REF)) { + continue; + } + Enum.Builder enumBuilder = Enum.newBuilder() + .setName(enumClassInfo.simpleName()) + .setExport(exported); + if (enumClassInfo.isEnum()) { + decls.add(extractValueEnum(enumClassInfo, enumBuilder)); } else { - String aString = (String) value.get(constant); - valueBuilder.setStringValue(StringValue.newBuilder().setValue(aString).build()); + // Type enums + var variants = index.getComputingIndex().getAllKnownImplementors(enumClassInfo.name()); + if (variants.isEmpty()) { + throw new RuntimeException("No variants found for enum: " + enumBuilder.getName()); + } + for (var variant : variants) { + if (variant.hasDeclaredAnnotation(GENERATED_REF)) { + continue; + } + Type variantType = ClassType.builder(variant.name()).build(); + xyz.block.ftl.v1.schema.Type declType = moduleBuilder.buildType(variantType, exported); + 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()); + } + decls.add(Decl.newBuilder().setEnum(enumBuilder).build()); } - EnumVariant variant = EnumVariant.newBuilder() - .setName(constant.toString()) - .setValue(valueBuilder) - .build(); - enumBuilder.addVariants(variant); } - // TODO move outside if - decls.add(Decl.newBuilder().setEnum(enumBuilder).build()); - } else { - // Type enums - // TODO + for (var decl : decls) { + moduleBuilder.addDecls(decl); + } + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); } + } + }); + } + + private Decl extractValueEnum(ClassInfo enumClassInfo, Enum.Builder enumBuilder) + throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + // Value enums must have a type + FieldInfo valueField = enumClassInfo.field("value"); + if (valueField == null) { + throw new RuntimeException("Enum must have a 'value' field: " + enumClassInfo.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: " + enumClassInfo.name()); + } + enumBuilder.setType(typeBuilder.build()); + Class enumClass = Class.forName(enumClassInfo.name().toString(), false, + Thread.currentThread().getContextClassLoader()); + for (var constant : enumClass.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()); } - return new SchemaContributorBuildItem(decls); - } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); + EnumVariant variant = EnumVariant.newBuilder() + .setName(constant.toString()) + .setValue(valueBuilder) + .build(); + enumBuilder.addVariants(variant); } + return Decl.newBuilder().setEnum(enumBuilder).build(); } private boolean isInt(Type type) { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index 78abc19857..c84a23da1b 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -345,14 +345,11 @@ public Type buildType(org.jboss.jandex.Type type, boolean export) { return Type.newBuilder().setTime(Time.newBuilder().build()).build(); } - var ref = Type.newBuilder().setRef(Ref.newBuilder() - .setName(clazz.name().local()) - .setModule(moduleName) - .build()) - .build(); + var ref = Type.newBuilder().setRef( + Ref.newBuilder().setName(clazz.name().local()).setModule(moduleName).build()).build(); if (info.isEnum() || info.hasAnnotation(ENUM)) { - // We set only the name and export here. EnumProcessor will fill in the rest + // Set only the name and export here. EnumProcessor will fill in the rest xyz.block.ftl.v1.schema.Enum ennum = xyz.block.ftl.v1.schema.Enum.newBuilder() .setName(clazz.name().local()) .setExport(type.hasAnnotation(EXPORT) || export) @@ -360,11 +357,10 @@ public Type buildType(org.jboss.jandex.Type type, boolean export) { addDecls(Decl.newBuilder().setEnum(ennum).build()); return ref; } else { - // If we've processed this data already, skip early + // If this data was processed already, skip early if (updateData(clazz.name().local(), type.hasAnnotation(EXPORT) || export)) { return ref; } - Data.Builder data = Data.newBuilder(); data.setName(clazz.name().local()); data.setExport(type.hasAnnotation(EXPORT) || export); @@ -529,12 +525,10 @@ private boolean updateEnum(String name, Decl decl) { duplicateNameValidationError(name, decl.getEnum().getPos()); } var moreComplete = decl.getEnum().getVariantsCount() > 0 ? decl : existing; - var merged = existing.getEnum().toBuilder() - .setName(moreComplete.getEnum().getName()) - .setExport(decl.getEnum().getExport() || existing.getEnum().getExport()) - .addAllVariants(moreComplete.getEnum().getVariantsList()) - .addAllComments(moreComplete.getEnum().getCommentsList()) - .setType(moreComplete.getEnum().getType()).build(); + var lessComplete = decl.getEnum().getVariantsCount() > 0 ? existing : decl; + var merged = moreComplete.getEnum().toBuilder() + .setExport(lessComplete.getEnum().getExport() || existing.getEnum().getExport()) + .build(); decls.put(name, Decl.newBuilder().setEnum(merged).build()); return true; } diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Animal.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Animal.java new file mode 100644 index 0000000000..bb11aa0584 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Animal.java @@ -0,0 +1,14 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.Enum; + +@Enum +public interface Animal { + public boolean isCat(); + + public boolean isDog(); + + public Cat getCat(); + + public Dog getDog(); +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java new file mode 100644 index 0000000000..03960a24c6 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java @@ -0,0 +1,22 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.EnumVariant; + +@EnumVariant +public class Cat implements Animal { + public boolean isCat() { + return true; + } + + public boolean isDog() { + return false; + } + + public Cat getCat() { + return this; + } + + public Dog getDog() { + return null; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java new file mode 100644 index 0000000000..d59ea4b8b5 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java @@ -0,0 +1,22 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.EnumVariant; + +@EnumVariant +public class Dog implements Animal { + public boolean isCat() { + return false; + } + + public boolean isDog() { + return true; + } + + public Cat getCat() { + return null; + } + + public Dog getDog() { + return this; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java index 3873dbfa5f..6a2e528ddb 100644 --- a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java @@ -32,4 +32,10 @@ public ColorInt valueEnumVerb(ColorInt color) { public Shape stringEnumVerb(Shape shape) { return shape; } + + @Export + @Verb + public Animal typeEnumVerb(Animal animal) { + return animal; + } } From 2be467112047cffa719296e7fbfcd763be01440d Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 2 Oct 2024 16:10:04 +1000 Subject: [PATCH 16/34] Extract schema from type enum holder classes --- .../xyz/block/ftl/deployment/EnumProcessor.java | 14 +++++++++++++- .../xyz/block/ftl/deployment/FTLDotNames.java | 2 ++ .../xyz/block/ftl/deployment/ModuleBuilder.java | 12 +++++++++++- .../src/main/java/xyz/block/ftl/EnumHolder.java | 14 ++++++++++++++ .../src/main/java/xyz/block/ftl/enums/Cat.java | 3 --- .../src/main/java/xyz/block/ftl/enums/Dog.java | 3 --- .../src/main/java/xyz/block/ftl/enums/List.java | 16 ++++++++++++++++ .../main/java/xyz/block/ftl/enums/Scalar.java | 16 ++++++++++++++++ .../java/xyz/block/ftl/enums/ScalarOrList.java | 8 ++++++++ .../xyz/block/ftl/javacomments/EnumType.java | 14 +++++++++++++- 10 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/EnumHolder.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/List.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Scalar.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ScalarOrList.java diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index 1b06e67f3f..e43cdc3804 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -4,6 +4,7 @@ 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; @@ -66,7 +67,18 @@ public void accept(ModuleBuilder moduleBuilder) { if (variant.hasDeclaredAnnotation(GENERATED_REF)) { continue; } - Type variantType = ClassType.builder(variant.name()).build(); + 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(); + } else { + // Class is the enum variant type + variantType = ClassType.builder(variant.name()).build(); + } xyz.block.ftl.v1.schema.Type declType = moduleBuilder.buildType(variantType, exported); TypeValue typeValue = TypeValue.newBuilder().setValue(declType).build(); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java index 23efa4c783..2bc6d65a46 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java @@ -5,6 +5,7 @@ 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; @@ -24,6 +25,7 @@ private FTLDotNames() { 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); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index c84a23da1b..b0b2bfa2de 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -526,10 +526,20 @@ private boolean updateEnum(String name, Decl decl) { } var moreComplete = decl.getEnum().getVariantsCount() > 0 ? decl : existing; var lessComplete = decl.getEnum().getVariantsCount() > 0 ? existing : decl; + boolean export = lessComplete.getEnum().getExport() || existing.getEnum().getExport(); var merged = moreComplete.getEnum().toBuilder() - .setExport(lessComplete.getEnum().getExport() || existing.getEnum().getExport()) + .setExport(export) .build(); decls.put(name, Decl.newBuilder().setEnum(merged).build()); + if (export && !existing.getEnum().getExport()) { + // If the existing enum was not exported, we need to update variants too + for (var childDecl : merged.getVariantsList()) { + if (childDecl.getValue().hasTypeValue() && childDecl.getValue().getTypeValue().getValue().hasRef()) { + var ref = childDecl.getValue().getTypeValue().getValue().getRef(); + updateData(ref.getName(), true); + } + } + } return true; } return false; diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/EnumHolder.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/EnumHolder.java new file mode 100644 index 0000000000..8ab7f45ed6 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/EnumHolder.java @@ -0,0 +1,14 @@ +package xyz.block.ftl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a class as holder for an enum variant with a primitive type + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface EnumHolder { +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java index 03960a24c6..85c5c56c6f 100644 --- a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Cat.java @@ -1,8 +1,5 @@ package xyz.block.ftl.enums; -import xyz.block.ftl.EnumVariant; - -@EnumVariant public class Cat implements Animal { public boolean isCat() { return true; diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java index d59ea4b8b5..fcd41680e6 100644 --- a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Dog.java @@ -1,8 +1,5 @@ package xyz.block.ftl.enums; -import xyz.block.ftl.EnumVariant; - -@EnumVariant public class Dog implements Animal { public boolean isCat() { return false; diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/List.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/List.java new file mode 100644 index 0000000000..e97dac4601 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/List.java @@ -0,0 +1,16 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.EnumHolder; + +@EnumHolder +public final class List implements ScalarOrList { + public final java.util.List value; + + public List() { + this.value = null; + } + + public List(java.util.List value) { + this.value = value; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Scalar.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Scalar.java new file mode 100644 index 0000000000..8fc4698cfe --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Scalar.java @@ -0,0 +1,16 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.EnumHolder; + +@EnumHolder +public final class Scalar implements ScalarOrList { + public final String value; + + public Scalar() { + this.value = null; + } + + public Scalar(String value) { + this.value = value; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ScalarOrList.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ScalarOrList.java new file mode 100644 index 0000000000..c985ff2ab8 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ScalarOrList.java @@ -0,0 +1,8 @@ +package xyz.block.ftl.enums; + +import xyz.block.ftl.Enum; + +@Enum +public sealed interface ScalarOrList permits Scalar, List { + +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/EnumType.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/EnumType.java index 2c36f42822..f9508dbe96 100644 --- a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/EnumType.java +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/javacomments/EnumType.java @@ -1,14 +1,26 @@ package xyz.block.ftl.javacomments; +import xyz.block.ftl.Enum; import xyz.block.ftl.Export; /** * Comment on an enum type */ +@Enum @Export public enum EnumType { /** * Comment on an enum value */ - PORTENTOUS + PORTENTOUS("portentous"); + + private final String value; + + EnumType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } From fdfbc8941688f453005514d4d075b32e4b99f125 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 2 Oct 2024 16:34:13 +1000 Subject: [PATCH 17/34] Extract schema from type enum holder classes --- .../main/java/xyz/block/ftl/deployment/ModuleBuilder.java | 4 ++-- .../block/ftl/javalang/deployment/JavaCodeGenerator.java | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index b0b2bfa2de..f6cb76e1c4 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -531,8 +531,8 @@ private boolean updateEnum(String name, Decl decl) { .setExport(export) .build(); decls.put(name, Decl.newBuilder().setEnum(merged).build()); - if (export && !existing.getEnum().getExport()) { - // If the existing enum was not exported, we need to update variants too + if (export) { + // Need to update export on variants too for (var childDecl : merged.getVariantsList()) { if (childDecl.getValue().hasTypeValue() && childDecl.getValue().getTypeValue().getValue().hasRef()) { var ref = childDecl.getValue().getTypeValue().getValue().getRef(); diff --git a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java index 05966c3d73..e05e25ac32 100644 --- a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java @@ -27,6 +27,7 @@ import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.WildcardTypeName; +import xyz.block.ftl.EnumHolder; import xyz.block.ftl.GeneratedRef; import xyz.block.ftl.Subscription; import xyz.block.ftl.TypeAlias; @@ -170,6 +171,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map Date: Fri, 4 Oct 2024 12:42:52 +1000 Subject: [PATCH 18/34] Serialise/deserialize value enums --- .../block/ftl/deployment/EnumProcessor.java | 37 +++--- .../block/ftl/deployment/ModuleBuilder.java | 4 - .../xyz/block/ftl/runtime/FTLRecorder.java | 8 ++ .../ftl/runtime/JsonSerializationConfig.java | 119 ++++++++++++++++++ jvm-runtime/testdata/go/gomodule/server.go | 4 +- .../block/ftl/test/TestInvokeGoFromJava.java | 30 +++-- .../xyz/block/ftl/enums/AnimalWrapper.java | 23 ++++ .../xyz/block/ftl/enums/ColorWrapper.java | 27 ++++ .../xyz/block/ftl/enums/ShapeWrapper.java | 23 ++++ .../main/java/xyz/block/ftl/enums/Verbs.java | 6 +- 10 files changed, 248 insertions(+), 33 deletions(-) create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/AnimalWrapper.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorWrapper.java create mode 100644 jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ShapeWrapper.java diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index e43cdc3804..d72823d463 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -22,7 +22,10 @@ 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; @@ -37,7 +40,8 @@ public class EnumProcessor { private static final Logger log = LoggerFactory.getLogger(EnumProcessor.class); @BuildStep - SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index) { + @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()); @@ -48,18 +52,21 @@ public void accept(ModuleBuilder moduleBuilder) { try { for (var enumAnnotation : enumAnnotations) { boolean exported = enumAnnotation.target().hasAnnotation(FTLDotNames.EXPORT); - ClassInfo enumClassInfo = enumAnnotation.target().asClass(); - if (enumClassInfo.hasDeclaredAnnotation(GENERATED_REF)) { - continue; - } + ClassInfo classInfo = enumAnnotation.target().asClass(); + Class clazz = Class.forName(classInfo.name().toString(), false, + Thread.currentThread().getContextClassLoader()); + var isLocalToModule = !classInfo.hasDeclaredAnnotation(GENERATED_REF); Enum.Builder enumBuilder = Enum.newBuilder() - .setName(enumClassInfo.simpleName()) + .setName(classInfo.simpleName()) .setExport(exported); - if (enumClassInfo.isEnum()) { - decls.add(extractValueEnum(enumClassInfo, enumBuilder)); + if (classInfo.isEnum()) { + recorder.registerEnum(clazz); + if (isLocalToModule) { + decls.add(extractValueEnum(classInfo, clazz, enumBuilder)); + } } else { // Type enums - var variants = index.getComputingIndex().getAllKnownImplementors(enumClassInfo.name()); + var variants = index.getComputingIndex().getAllKnownImplementors(classInfo.name()); if (variants.isEmpty()) { throw new RuntimeException("No variants found for enum: " + enumBuilder.getName()); } @@ -100,12 +107,12 @@ public void accept(ModuleBuilder moduleBuilder) { }); } - private Decl extractValueEnum(ClassInfo enumClassInfo, Enum.Builder enumBuilder) + private Decl extractValueEnum(ClassInfo classInfo, Class clazz, Enum.Builder enumBuilder) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { // Value enums must have a type - FieldInfo valueField = enumClassInfo.field("value"); + FieldInfo valueField = classInfo.field("value"); if (valueField == null) { - throw new RuntimeException("Enum must have a 'value' field: " + enumClassInfo.name()); + 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(); @@ -115,13 +122,11 @@ private Decl extractValueEnum(ClassInfo enumClassInfo, Enum.Builder enumBuilder) 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: " + enumClassInfo.name()); + "Enum value type must be String, int, long, short, or byte: " + classInfo.name()); } enumBuilder.setType(typeBuilder.build()); - Class enumClass = Class.forName(enumClassInfo.name().toString(), false, - Thread.currentThread().getContextClassLoader()); - for (var constant : enumClass.getEnumConstants()) { + for (var constant : clazz.getEnumConstants()) { Field value = constant.getClass().getDeclaredField("value"); value.setAccessible(true); Value.Builder valueBuilder = Value.newBuilder(); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index f6cb76e1c4..11806fdbc0 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -568,10 +568,6 @@ private void duplicateNameValidationError(String name, Position pos) { name, moduleName, pos.getFilename() + ":" + pos.getLine()))); } - record ExistingRef(Ref ref, boolean exported) { - - } - public enum BodyType { DISALLOWED, ALLOWED, diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java index 5fd81a623d..0185e3cd58 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java @@ -46,6 +46,14 @@ public void registerHttpIngress(String module, String verbName, boolean base64En } } + public void registerEnum(Class ennum) { + try { + Arc.container().instance(JsonSerializationConfig.class).get().registerValueEnum(ennum); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public BiFunction topicSupplier(String className, String callingVerb) { try { var cls = Thread.currentThread().getContextClassLoader().loadClass(className.replace("/", ".")); diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java index ff8e683671..503837541c 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java @@ -1,15 +1,22 @@ package xyz.block.ftl.runtime; import java.io.IOException; +import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; +import java.util.ArrayList; import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import org.jboss.logging.Logger; + import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -37,6 +44,9 @@ public class JsonSerializationConfig implements ObjectMapperCustomizer { final Instance> instances; + private record EnumCereal(Class clazz, EnumDeserializer deserializer, EnumSerializer serializer) { } + final List enumList = new ArrayList<>(); + @Inject public JsonSerializationConfig(Instance> instances) { this.instances = instances; @@ -55,9 +65,22 @@ public void customize(ObjectMapper mapper) { module.addSerializer(object, new TypeAliasSerializer(object, serialized, i)); module.addDeserializer(object, new TypeAliasDeSerializer(object, serialized, i)); } + for (var i : enumList) { + module.addSerializer(i.clazz, i.serializer); + module.addDeserializer(i.clazz, i.deserializer); + } mapper.registerModule(module); } + public > void registerValueEnum(Class enumClass) { + enumList.add(new EnumCereal(enumClass, new EnumDeserializer(enumClass), new EnumSerializer(enumClass))); + } + + // public void registerEnum(String module, Class enumClass) { + // new EnumSerializer(enumClass, new ValueEnumMapper(enumClass)); + // enumList.add(enumClass); + // } + static Class extractTypeAliasParam(Class target, int no) { return (Class) extractTypeAliasParamImpl(target, no); } @@ -156,4 +179,100 @@ public void serialize(T value, JsonGenerator gen, SerializerProvider provider) t } } +// // public record WireEnum(String name, Object value) { +// // } +// +// public interface EnumMapper { +// // Object serialize(T value); +// +// T deserialize(Object value); +// } +// +// static class ValueEnumMapper implements EnumMapper { +// +// private final Map lookup = new HashMap<>(); +// +// public ValueEnumMapper(Class type) { +// +// for (T ennum : type.getEnumConstants()) { +// try { +// Field valueField = ennum.getClass().getDeclaredField("value"); +// valueField.setAccessible(true); +// lookup.put(valueField.get(ennum), ennum); +// } catch (NoSuchFieldException | IllegalAccessException e) { +// throw new RuntimeException(e); +// } +// } +// } +// +// @Override +// public WireEnum serialize(T value) { +// try { +// Field valueField = value.getClass().getDeclaredField("value"); +// valueField.setAccessible(true); +// WireEnum wireEnum = new WireEnum(type.getSimpleName(), valueField.get(value)); +// log.warn("Value enum mapping enum " + value + " to wire " + wireEnum); +// return wireEnum; +// } catch (NoSuchFieldException | IllegalAccessException e) { +// throw new RuntimeException(e); +// } +// } +// +// @Override +// public T deserialize(Object value) { +// T ennum = lookup.get(value); +// // ennum. +// // T ennum = Enum.valueOf(type, (String) value.value); +// log.warn("Value enum mapping wire value " + value + " to " + ennum); +// return ennum; +// } +// } + + public static class EnumSerializer extends StdSerializer { + private final Field valueField; + + public EnumSerializer(Class type) { + super(type); + try { + this.valueField = type.getDeclaredField("value"); + valueField.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + @Override + public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException { + try { + gen.writeObject(valueField.get(value)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + public static class EnumDeserializer extends StdDeserializer { + private final Map wireToEnum = new HashMap<>(); + private final Class valueClass; + + public EnumDeserializer(Class type) { + super(type); + try { + Field valueField = type.getDeclaredField("value"); + valueField.setAccessible(true); + valueClass = valueField.getType(); + for (T ennum : type.getEnumConstants()) { + wireToEnum.put(valueField.get(ennum), ennum); + } + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Object wireVal = ctxt.readValue(p, valueClass); + return wireToEnum.get(wireVal); + } + } } diff --git a/jvm-runtime/testdata/go/gomodule/server.go b/jvm-runtime/testdata/go/gomodule/server.go index e27996f848..541a24d9d0 100644 --- a/jvm-runtime/testdata/go/gomodule/server.go +++ b/jvm-runtime/testdata/go/gomodule/server.go @@ -259,7 +259,7 @@ func ValueEnumVerb(ctx context.Context, val ColorWrapper) (ColorWrapper, error) } //ftl:verb export -func ShapeEnumVerb(ctx context.Context, val ShapeWrapper) (ShapeWrapper, error) { +func StringEnumVerb(ctx context.Context, val ShapeWrapper) (ShapeWrapper, error) { return val, nil } @@ -283,7 +283,7 @@ func GetAnimal(ctx context.Context) (AnimalWrapper, error) { // return val, nil //} -func callJavaServer(ctx context.Context, req javaserver.ColorInt, getValueEnum javaserver.ValueEnumVerbClient) (javaserver.ColorInt, error) { +func callJavaServer(ctx context.Context, req javaserver.ColorWrapper, getValueEnum javaserver.ValueEnumVerbClient) (javaserver.ColorWrapper, error) { color, _ := getValueEnum(ctx, req) return color, nil } diff --git a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index 12cc96134c..c12c1cc971 100644 --- a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -229,12 +229,14 @@ public Did externalTypeVerb(Did val, ExternalTypeVerbClient client) { @Export @Verb - public Animal noValueTypeEnumVerb(Animal animal, NoValueTypeEnumVerbClient client) { - if (animal.isCat()) { - return client.call(animal.getCat()); + public AnimalWrapper noValueTypeEnumVerb(AnimalWrapper animal, NoValueTypeEnumVerbClient client) { + if (animal.getAnimal().isCat()) { + return client.call(new AnimalWrapper(animal.getAnimal().getCat())); } else { return client.call(animal.getDog()); } + return client.call(new AnimalWrapper(animal.getAnimal().getDog())); + } } @Export @@ -245,11 +247,23 @@ public ColorInt valueEnumVerb(ColorInt color, ValueEnumVerbClient client) { @Export @Verb - public TypeEnum typeEnumVerb(TypeEnum value, TypeEnumVerbClient client) { - if (value.isScalar()) { - return client.call(new StringList(List.of("a", "b", "c"))); - } else if (value.isStringList()) { - return client.call(new Scalar("scalar")); + public ColorWrapper valueEnumVerb(ColorWrapper color, ValueEnumVerbClient client) { + return client.call(color); + } + + @Export + @Verb + public ShapeWrapper stringEnumVerb(ShapeWrapper shape, StringEnumVerbClient client) { + return client.call(shape); + } + + @Export + @Verb + public TypeEnumWrapper typeEnumVerb(TypeEnumWrapper value, TypeEnumVerbClient client) { + if (value.getType().isScalar()) { + return client.call(new TypeEnumWrapper(new StringList(List.of("a", "b", "c")))); + } else if (value.getType().isStringList()) { + return client.call(new TypeEnumWrapper(new Scalar("scalar"))); } else { throw new IllegalArgumentException("unexpected value"); } diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/AnimalWrapper.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/AnimalWrapper.java new file mode 100644 index 0000000000..10e40bc6a6 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/AnimalWrapper.java @@ -0,0 +1,23 @@ +package xyz.block.ftl.enums; + +import org.jetbrains.annotations.NotNull; + +public class AnimalWrapper { + private @NotNull Animal animal; + + public AnimalWrapper() { + } + + public AnimalWrapper(@NotNull Animal animal) { + this.animal = animal; + } + + public AnimalWrapper setAnimal(@NotNull Animal animal) { + this.animal = animal; + return this; + } + + public @NotNull Animal getAnimal() { + return animal; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorWrapper.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorWrapper.java new file mode 100644 index 0000000000..797f854e6a --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ColorWrapper.java @@ -0,0 +1,27 @@ +package xyz.block.ftl.enums; + +import org.jetbrains.annotations.NotNull; + +public class ColorWrapper { + private @NotNull ColorInt color; + + public ColorWrapper() { + } + + public ColorWrapper(@NotNull ColorInt color) { + this.color = color; + } + + public ColorWrapper setColor(@NotNull ColorInt color) { + this.color = color; + return this; + } + + public @NotNull ColorInt getColor() { + return color; + } + + public String toString() { + return "ColorWrapper(color=" + this.color + ")"; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ShapeWrapper.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ShapeWrapper.java new file mode 100644 index 0000000000..76ba6a6073 --- /dev/null +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/ShapeWrapper.java @@ -0,0 +1,23 @@ +package xyz.block.ftl.enums; + +import org.jetbrains.annotations.NotNull; + +public class ShapeWrapper { + private @NotNull Shape shape; + + public ShapeWrapper() { + } + + public ShapeWrapper(@NotNull Shape shape) { + this.shape = shape; + } + + public ShapeWrapper setShape(@NotNull Shape shape) { + this.shape = shape; + return this; + } + + public @NotNull Shape getShape() { + return shape; + } +} diff --git a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java index 6a2e528ddb..fbfce42866 100644 --- a/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java +++ b/jvm-runtime/testdata/java/javaserver/src/main/java/xyz/block/ftl/enums/Verbs.java @@ -23,19 +23,19 @@ public Object anyOutput(String name) { @Export @Verb - public ColorInt valueEnumVerb(ColorInt color) { + public ColorWrapper valueEnumVerb(ColorWrapper color) { return color; } @Export @Verb - public Shape stringEnumVerb(Shape shape) { + public ShapeWrapper stringEnumVerb(ShapeWrapper shape) { return shape; } @Export @Verb - public Animal typeEnumVerb(Animal animal) { + public AnimalWrapper typeEnumVerb(AnimalWrapper animal) { return animal; } } From 8836dec584cba7429e6d8c7932b856b081c79477 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Tue, 8 Oct 2024 15:08:00 +1000 Subject: [PATCH 19/34] Serialise/deserialize type enums --- .../block/ftl/deployment/EnumProcessor.java | 27 ++-- .../xyz/block/ftl/runtime/FTLRecorder.java | 8 + .../ftl/runtime/JsonSerializationConfig.java | 140 +++++++++--------- .../java/xyz/block/ftl/runtime/Animal.java | 17 +++ .../test/java/xyz/block/ftl/runtime/Cat.java | 44 ++++++ .../java/xyz/block/ftl/runtime/ColorInt.java | 20 +++ .../test/java/xyz/block/ftl/runtime/Dog.java | 16 ++ .../runtime/JsonSerializationConfigTest.java | 49 ++++++ .../java/xyz/block/ftl/runtime/Shape.java | 20 +++ .../deployment/JavaCodeGenerator.java | 8 + jvm-runtime/testdata/go/gomodule/server.go | 25 ++-- .../block/ftl/test/TestInvokeGoFromJava.java | 22 +-- .../main/java/xyz/block/ftl/enums/Animal.java | 14 +- .../main/java/xyz/block/ftl/enums/Cat.java | 29 ++++ 14 files changed, 327 insertions(+), 112 deletions(-) create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Animal.java create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Cat.java create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/ColorInt.java create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Dog.java create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Shape.java diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index d72823d463..e4a8c6d4e3 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -67,13 +67,12 @@ public void accept(ModuleBuilder moduleBuilder) { } else { // Type enums var variants = index.getComputingIndex().getAllKnownImplementors(classInfo.name()); + var variantClasses = new ArrayList>(); if (variants.isEmpty()) { throw new RuntimeException("No variants found for enum: " + enumBuilder.getName()); } for (var variant : variants) { - if (variant.hasDeclaredAnnotation(GENERATED_REF)) { - continue; - } + var isVariantLocalToModule = !variant.hasDeclaredAnnotation(GENERATED_REF); Type variantType; if (variant.hasAnnotation(ENUM_HOLDER)) { // Enum value holder class @@ -85,16 +84,24 @@ public void accept(ModuleBuilder moduleBuilder) { } 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); - TypeValue typeValue = TypeValue.newBuilder().setValue(declType).build(); + if (isVariantLocalToModule) { + xyz.block.ftl.v1.schema.Type declType = moduleBuilder.buildType(variantType, exported); + 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()); + EnumVariant.Builder variantBuilder = EnumVariant.newBuilder() + .setName(variant.simpleName()) + .setValue(Value.newBuilder().setTypeValue(typeValue).build()); + enumBuilder.addVariants(variantBuilder.build()); + } + } + if (isLocalToModule) { + decls.add(Decl.newBuilder().setEnum(enumBuilder).build()); } - decls.add(Decl.newBuilder().setEnum(enumBuilder).build()); + recorder.registerEnum(clazz, variantClasses); } } for (var decl : decls) { diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java index 0185e3cd58..a3a88a46c4 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java @@ -54,6 +54,14 @@ public void registerEnum(Class ennum) { } } + public void registerEnum(Class ennum, List> variants) { + try { + Arc.container().instance(JsonSerializationConfig.class).get().registerTypeEnum(ennum, variants); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public BiFunction topicSupplier(String className, String callingVerb) { try { var cls = Thread.currentThread().getContextClassLoader().loadClass(className.replace("/", ".")); diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java index 503837541c..b92a75d5b4 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java @@ -15,8 +15,6 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; -import org.jboss.logging.Logger; - import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -29,6 +27,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.quarkus.arc.Unremovable; @@ -42,16 +41,23 @@ @Unremovable public class JsonSerializationConfig implements ObjectMapperCustomizer { - final Instance> instances; + final Iterable> instances; + + private record TypeEnumDefn(Class type, List> variants) { + } - private record EnumCereal(Class clazz, EnumDeserializer deserializer, EnumSerializer serializer) { } - final List enumList = new ArrayList<>(); + final List valueEnums = new ArrayList<>(); + final List typeEnums = new ArrayList<>(); @Inject public JsonSerializationConfig(Instance> instances) { this.instances = instances; } + JsonSerializationConfig() { + this.instances = List.of(); + } + @Override public void customize(ObjectMapper mapper) { mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); @@ -65,21 +71,26 @@ public void customize(ObjectMapper mapper) { module.addSerializer(object, new TypeAliasSerializer(object, serialized, i)); module.addDeserializer(object, new TypeAliasDeSerializer(object, serialized, i)); } - for (var i : enumList) { - module.addSerializer(i.clazz, i.serializer); - module.addDeserializer(i.clazz, i.deserializer); + for (var i : valueEnums) { + module.addSerializer(i, new ValueEnumSerializer(i)); + module.addDeserializer(i, new ValueEnumDeserializer(i)); + } + + ObjectMapper cleanMapper = mapper.copy(); + for (var i : typeEnums) { + module.addSerializer(i.type, new TypeEnumSerializer<>(i.type, cleanMapper)); + module.addDeserializer(i.type, new TypeEnumDeserializer<>(i.type, i.variants)); } mapper.registerModule(module); } public > void registerValueEnum(Class enumClass) { - enumList.add(new EnumCereal(enumClass, new EnumDeserializer(enumClass), new EnumSerializer(enumClass))); + valueEnums.add(enumClass); } - // public void registerEnum(String module, Class enumClass) { - // new EnumSerializer(enumClass, new ValueEnumMapper(enumClass)); - // enumList.add(enumClass); - // } + public void registerTypeEnum(Class type, List> variants) { + typeEnums.add(new TypeEnumDefn<>(type, variants)); + } static Class extractTypeAliasParam(Class target, int no) { return (Class) extractTypeAliasParamImpl(target, no); @@ -139,7 +150,6 @@ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx String base64 = node.asText(); return Base64.getDecoder().decode(base64); } - } public static class TypeAliasDeSerializer extends StdDeserializer { @@ -158,7 +168,6 @@ public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOExcepti var s = ctxt.readValue(p, serializedType); return mapper.decode(s); } - } public static class TypeAliasSerializer extends StdSerializer { @@ -179,59 +188,10 @@ public void serialize(T value, JsonGenerator gen, SerializerProvider provider) t } } -// // public record WireEnum(String name, Object value) { -// // } -// -// public interface EnumMapper { -// // Object serialize(T value); -// -// T deserialize(Object value); -// } -// -// static class ValueEnumMapper implements EnumMapper { -// -// private final Map lookup = new HashMap<>(); -// -// public ValueEnumMapper(Class type) { -// -// for (T ennum : type.getEnumConstants()) { -// try { -// Field valueField = ennum.getClass().getDeclaredField("value"); -// valueField.setAccessible(true); -// lookup.put(valueField.get(ennum), ennum); -// } catch (NoSuchFieldException | IllegalAccessException e) { -// throw new RuntimeException(e); -// } -// } -// } -// -// @Override -// public WireEnum serialize(T value) { -// try { -// Field valueField = value.getClass().getDeclaredField("value"); -// valueField.setAccessible(true); -// WireEnum wireEnum = new WireEnum(type.getSimpleName(), valueField.get(value)); -// log.warn("Value enum mapping enum " + value + " to wire " + wireEnum); -// return wireEnum; -// } catch (NoSuchFieldException | IllegalAccessException e) { -// throw new RuntimeException(e); -// } -// } -// -// @Override -// public T deserialize(Object value) { -// T ennum = lookup.get(value); -// // ennum. -// // T ennum = Enum.valueOf(type, (String) value.value); -// log.warn("Value enum mapping wire value " + value + " to " + ennum); -// return ennum; -// } -// } - - public static class EnumSerializer extends StdSerializer { + public static class ValueEnumSerializer extends StdSerializer { private final Field valueField; - public EnumSerializer(Class type) { + public ValueEnumSerializer(Class type) { super(type); try { this.valueField = type.getDeclaredField("value"); @@ -251,11 +211,11 @@ public void serialize(T value, JsonGenerator gen, SerializerProvider provider) t } } - public static class EnumDeserializer extends StdDeserializer { + public static class ValueEnumDeserializer extends StdDeserializer { private final Map wireToEnum = new HashMap<>(); private final Class valueClass; - public EnumDeserializer(Class type) { + public ValueEnumDeserializer(Class type) { super(type); try { Field valueField = type.getDeclaredField("value"); @@ -275,4 +235,48 @@ public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOExcepti return wireToEnum.get(wireVal); } } + + public static class TypeEnumSerializer extends StdSerializer { + private final ObjectMapper defaultMapper; + + public TypeEnumSerializer(Class type, ObjectMapper mapper) { + super(type); + defaultMapper = mapper; + } + + @Override + public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + gen.writeStringField("name", value.getClass().getSimpleName()); + gen.writeFieldName("value"); + // Avoid infinite recursion by using a mapper without this serializer registered + defaultMapper.writeValue(gen, value); + gen.writeEndObject(); + } + } + + public static class TypeEnumDeserializer extends StdDeserializer { + private final Map> nameToVariant = new HashMap<>(); + + public TypeEnumDeserializer(Class type, List> variants) { + super(type); + for (var variant : variants) { + nameToVariant.put(variant.getSimpleName(), variant); + } + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + ObjectNode wireValue = p.readValueAsTree(); + if (!wireValue.has("name") || !wireValue.has("value")) { + throw new RuntimeException("Enum missing 'name' or 'value' fields"); + } + String name = wireValue.get("name").asText(); + Class variant = nameToVariant.get(name); + if (variant == null) { + throw new RuntimeException("Unknown variant " + name); + } + return (T) wireValue.get("value").traverse(p.getCodec()).readValueAs(variant); + } + } } diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Animal.java b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Animal.java new file mode 100644 index 0000000000..e3b17c0315 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Animal.java @@ -0,0 +1,17 @@ +package xyz.block.ftl.runtime; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import xyz.block.ftl.Enum; + +@Enum +public interface Animal { + @JsonIgnore + boolean isCat(); + + @JsonIgnore + boolean isDog(); + + @JsonIgnore + Cat getCat(); +} diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Cat.java b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Cat.java new file mode 100644 index 0000000000..6f9cd5ec33 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Cat.java @@ -0,0 +1,44 @@ +package xyz.block.ftl.runtime; + +import org.jetbrains.annotations.NotNull; + +public class Cat implements Animal { + private @NotNull String name; + + private @NotNull String breed; + + private long furLength; + + public Cat() { + } + + public Cat(@NotNull String breed, long furLength, @NotNull String name) { + this.breed = breed; + this.furLength = furLength; + this.name = name; + } + + public boolean isCat() { + return true; + } + + public boolean isDog() { + return false; + } + + public Cat getCat() { + return this; + } + + public @NotNull String getName() { + return name; + } + + public @NotNull String getBreed() { + return breed; + } + + public long getFurLength() { + return furLength; + } +} diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/ColorInt.java b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/ColorInt.java new file mode 100644 index 0000000000..f6d7b9181f --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/ColorInt.java @@ -0,0 +1,20 @@ +package xyz.block.ftl.runtime; + +import xyz.block.ftl.Enum; + +@Enum +public enum ColorInt { + RED(0), + GREEN(1), + BLUE(2); + + private final int value; + + ColorInt(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Dog.java b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Dog.java new file mode 100644 index 0000000000..d0c1711029 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Dog.java @@ -0,0 +1,16 @@ +package xyz.block.ftl.runtime; + +public class Dog implements Animal { + public boolean isCat() { + return false; + } + + public boolean isDog() { + return true; + } + + @Override + public Cat getCat() { + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/JsonSerializationConfigTest.java b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/JsonSerializationConfigTest.java index ea6f3f8f59..73a1b8adf6 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/JsonSerializationConfigTest.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/JsonSerializationConfigTest.java @@ -1,10 +1,14 @@ package xyz.block.ftl.runtime; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import xyz.block.ftl.TypeAliasMapper; class JsonSerializationConfigTest { @@ -27,6 +31,51 @@ public void testExtraction() { JsonSerializationConfig.extractTypeAliasParamImpl(AtomicIntTypeMapping.class, 1)); } + @Test + public void testTypeEnumSerialization() throws JsonProcessingException { + JsonSerializationConfig config = new JsonSerializationConfig(); + ObjectMapper mapper = new ObjectMapper(); + config.registerTypeEnum(Animal.class, List.of(Dog.class, Cat.class)); + config.customize(mapper); + + String serializedDog = mapper.writeValueAsString(new Dog()); + Assertions.assertEquals("{\"name\":\"Dog\",\"value\":{}}", serializedDog); + + Animal animal = mapper.readValue(serializedDog, Animal.class); + Assertions.assertTrue(animal instanceof Dog); + + String serializedCat = mapper.writeValueAsString(new Cat("Siamese", 10, "Fluffy")); + Assertions.assertEquals("{\"name\":\"Cat\",\"value\":{\"name\":\"Fluffy\",\"breed\":\"Siamese\",\"furLength\":10}}", + serializedCat); + + Animal cat = mapper.readValue(serializedCat, Animal.class); + Assertions.assertTrue(cat instanceof Cat); + Assertions.assertEquals("Fluffy", cat.getCat().getName()); + } + + @Test + public void testValueEnumSerialization() throws JsonProcessingException { + JsonSerializationConfig config = new JsonSerializationConfig(); + ObjectMapper mapper = new ObjectMapper(); + config.registerValueEnum(ColorInt.class); + config.registerValueEnum(Shape.class); + config.customize(mapper); + + String serializedRed = mapper.writeValueAsString(ColorInt.RED); + Assertions.assertEquals("0", serializedRed); + String serializedBlue = mapper.writeValueAsString(ColorInt.BLUE); + Assertions.assertEquals("2", serializedBlue); + + ColorInt deserialized = mapper.readValue(serializedBlue, ColorInt.class); + Assertions.assertEquals(ColorInt.BLUE, deserialized); + + String serializedCircle = mapper.writeValueAsString(Shape.CIRCLE); + Assertions.assertEquals("\"circle\"", serializedCircle); + + Shape deserializedShape = mapper.readValue(serializedCircle, Shape.class); + Assertions.assertEquals(Shape.CIRCLE, deserializedShape); + } + public static class AtomicIntTypeMapping implements TypeAliasMapper { @Override public Integer encode(AtomicInteger object) { diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Shape.java b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Shape.java new file mode 100644 index 0000000000..a7ffcf9ea7 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/Shape.java @@ -0,0 +1,20 @@ +package xyz.block.ftl.runtime; + +import xyz.block.ftl.Enum; + +@Enum +public enum Shape { + CIRCLE("circle"), + SQUARE("square"), + TRIANGLE("triangle"); + + private final String value; + + Shape(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java index e05e25ac32..7123bf7d86 100644 --- a/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/java/deployment/src/main/java/xyz/block/ftl/javalang/deployment/JavaCodeGenerator.java @@ -16,6 +16,7 @@ import org.jetbrains.annotations.NotNull; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ArrayTypeName; import com.squareup.javapoet.ClassName; @@ -122,6 +123,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map Date: Tue, 8 Oct 2024 16:20:24 +1000 Subject: [PATCH 20/34] update integration tests --- jvm-runtime/jvm_integration_test.go | 51 +++++++++++++++++++ .../block/ftl/test/TestInvokeGoFromJava.java | 11 ++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/jvm-runtime/jvm_integration_test.go b/jvm-runtime/jvm_integration_test.go index 6ac378cede..4fb00778c9 100644 --- a/jvm-runtime/jvm_integration_test.go +++ b/jvm-runtime/jvm_integration_test.go @@ -136,6 +136,17 @@ func TestJVMCoreFunctionality(t *testing.T) { tests = append(tests, PairedVerbTest("optionalTestObjectVerb", exampleObject)...) tests = append(tests, PairedVerbTest("optionalTestObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...) tests = append(tests, PairedVerbTest("externalTypeVerb", "did:web:abc123")...) + tests = append(tests, PairedVerbTest("typeEnumVerb", AnimalWrapper{Animal: Animal{ + Name: "Cat", + Value: Cat{ + Name: "Fluffy", + FurLength: 10, + Breed: "Siamese", + }, + }})...) + tests = append(tests, PairedVerbTest("valueEnumVerb", ColorWrapper{Color: Red})...) + //tests = append(tests, PairedVerbTest("typeWrapperEnumVerb", "hello")...) + //tests = append(tests, PairedVerbTest("mixedEnumVerb", Thing{})...) // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalIntVerb", ftl.None[int]())...) // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalFloatVerb", ftl.None[float64]())...) // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalStringVerb", ftl.None[string]())...) @@ -256,3 +267,43 @@ type ParameterizedType[T any] struct { Option ftl.Option[T] `json:"option"` Map map[string]T `json:"map"` } + +type ColorInt int + +const ( + Red ColorInt = 0 + Green ColorInt = 1 + Blue ColorInt = 2 +) + +type ColorWrapper struct { + Color ColorInt `json:"color"` +} + +type TypeWrapperEnum interface{ typeEnum() } +type Scalar string +type StringList []string + +func (Scalar) typeEnum() {} +func (StringList) typeEnum() {} + +type Animal struct { + Name string `json:"name"` + Value Cat `json:"value"` +} +type Cat struct { + Name string `json:"name"` + FurLength int `json:"furLength"` + Breed string `json:"breed"` +} + +type AnimalWrapper struct { + Animal Animal `json:"animal"` +} + +type Mixed interface{ tag() } +type Word string +type Thing struct{} + +func (Word) tag() {} +func (Thing) tag() {} diff --git a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index f3f6a472ee..1dbfd46ab9 100644 --- a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -232,11 +232,12 @@ public Did externalTypeVerb(Did val, ExternalTypeVerbClient client) { @Export @Verb public AnimalWrapper typeEnumVerb(AnimalWrapper animal, TypeEnumVerbClient client) { - if (animal.getAnimal().isCat()) { - return client.call(new AnimalWrapper(animal.getAnimal().getCat())); - } else { - return client.call(new AnimalWrapper(animal.getAnimal().getDog())); - } + return animal; +// if (animal.getAnimal().isCat()) { +// return client.call(new AnimalWrapper(animal.getAnimal().getCat())); +// } else { +// return client.call(new AnimalWrapper(animal.getAnimal().getDog())); +// } } @Export From 65e68b735ce8862f04536fb997ad4dfe741dd377 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Tue, 8 Oct 2024 16:51:47 +1000 Subject: [PATCH 21/34] fix merge mistakes --- .../src/main/java/xyz/block/ftl/deployment/EnumProcessor.java | 3 ++- .../main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index e4a8c6d4e3..3939ceb40c 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -89,7 +89,8 @@ public void accept(ModuleBuilder moduleBuilder) { variantClasses.add(variantClazz); } if (isVariantLocalToModule) { - xyz.block.ftl.v1.schema.Type declType = moduleBuilder.buildType(variantType, exported); + 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() diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java index 8571caba5e..bd2c57f723 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java @@ -1,8 +1,11 @@ package xyz.block.ftl.deployment; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.Type; import org.jboss.jandex.TypeVariable; import org.slf4j.Logger; From 7f964c224f4228026e8c4a6503ddcb94ff3cb257 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Tue, 8 Oct 2024 17:54:08 +1000 Subject: [PATCH 22/34] revert whitespace --- internal/lsp/hoveritems.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/lsp/hoveritems.go b/internal/lsp/hoveritems.go index 32c04a871c..e5cbd73ef1 100644 --- a/internal/lsp/hoveritems.go +++ b/internal/lsp/hoveritems.go @@ -5,8 +5,8 @@ var hoverMap = map[string]string{ "//ftl:cron": "## Cron\n\nA cron job is an Empty verb that will be called on a schedule. The syntax is described [here](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/crontab.html).\n\nYou can also use a shorthand syntax for the cron job, supporting seconds (`s`), minutes (`m`), hours (`h`), and specific days of the week (e.g. `Mon`).\n\n### Examples\n\nThe following function will be called hourly:\n\n```go\n//ftl:cron 0 * * * *\nfunc Hourly(ctx context.Context) error {\n // ...\n}\n```\nEvery 12 hours, starting at UTC midnight:\n\n```go\n//ftl:cron 12h\nfunc TwiceADay(ctx context.Context) error {\n // ...\n}\n```\n\nEvery Monday at UTC midnight:\n\n```go\n//ftl:cron Mon\nfunc Mondays(ctx context.Context) error {\n // ...\n}\n```", "//ftl:enum": "## Type enums (sum types)\n\n[Sum types](https://en.wikipedia.org/wiki/Tagged_union) are supported by FTL's type system, but aren't directly supported by Go. However they can be approximated with the use of [sealed interfaces](https://blog.chewxy.com/2018/03/18/golang-interfaces/). To declare a sum type in FTL use the comment directive `//ftl:enum`:\n\n```go\n//ftl:enum\ntype Animal interface { animal() }\n\ntype Cat struct {}\nfunc (Cat) animal() {}\n\ntype Dog struct {}\nfunc (Dog) animal() {}\n```\n## Value enums\n\nA value enum is an enumerated set of string or integer values.\n\n```go\n//ftl:enum\ntype Colour string\n\nconst (\n Red Colour = \"red\"\n Green Colour = \"green\"\n Blue Colour = \"blue\"\n)\n```\n", "//ftl:ingress": "## HTTP Ingress\n\nVerbs annotated with `ftl:ingress` will be exposed via HTTP (`http` is the default ingress type). These endpoints will then be available on one of our default `ingress` ports (local development defaults to `http://localhost:8891`).\n\nThe following will be available at `http://localhost:8891/http/users/123/posts?postId=456`.\n\n\n```go\ntype GetRequestPathParams struct {\n\tUserID string `json:\"userId\"`\n}\n\ntype GetRequestQueryParams struct {\n\tPostID string `json:\"postId\"`\n}\n\ntype GetResponse struct {\n\tMessage string `json:\"msg\"`\n}\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, GetRequestPathParams, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\nBecause the example above only has a single path parameter it can be simplified by just using a scalar such as `string` or `int64` as the path parameter type:\n\n```go\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, int64, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\n> **NOTE!**\n> The `req` and `resp` types of HTTP `ingress` [verbs](../verbs) must be `builtin.HttpRequest` and `builtin.HttpResponse` respectively. These types provide the necessary fields for HTTP `ingress` (`headers`, `statusCode`, etc.)\n>\n> You will need to import `ftl/builtin`.\n\nKey points:\n\n- `ingress` verbs will be automatically exported by default.\n\n## Field mapping\n\nThe `HttpRequest` request object takes 3 type parameters, the body, the path parameters and the query parameters.\n\nGiven the following request verb:\n\n```go\n\ntype PostBody struct{\n\tTitle string `json:\"title\"`\n\tContent string `json:\"content\"`\n\tTag ftl.Option[string] `json:\"tag\"`\n}\ntype PostPathParams struct {\n\tUserID string `json:\"userId\"`\n\tPostID string `json:\"postId\"`\n}\n\ntype PostQueryParams struct {\n\tPublish boolean `json:\"publish\"`\n}\n\n//ftl:ingress http PUT /users/{userId}/posts/{postId}\nfunc Get(ctx context.Context, req builtin.HttpRequest[PostBody, PostPathParams, PostQueryParams]) (builtin.HttpResponse[GetResponse, string], error) {\n\treturn builtin.HttpResponse[GetResponse, string]{\n\t\tHeaders: map[string][]string{\"Get\": {\"Header from FTL\"}},\n\t\tBody: ftl.Some(GetResponse{\n\t\t\tMessage: fmt.Sprintf(\"UserID: %s, PostID: %s, Tag: %s\", req.pathParameters.UserID, req.pathParameters.PostID, req.Body.Tag.Default(\"none\")),\n\t\t}),\n\t}, nil\n}\n```\n\nThe rules for how each element is mapped are slightly different, as they have a different structure:\n\n- The body is mapped directly to the body of the request, generally as a JSON object. Scalars are also supported, as well as []byte to get the raw body. If they type is `any` then it will be assumed to be JSON and mapped to the appropriate types based on the JSON structure.\n- The path parameters can be mapped directly to an object with field names corresponding to the name of the path parameter. If there is only a single path parameter it can be injected directly as a scalar. They can also be injected as a `map[string]string`.\n- The path parameters can also be mapped directly to an object with field names corresponding to the name of the path parameter. They can also be injected directly as a `map[string]string`, or `map[string][]string` for multiple values.\n\n#### Optional fields\n\nOptional fields are represented by the `ftl.Option` type. The `Option` type is a wrapper around the actual type and can be `Some` or `None`. In the example above, the `Tag` field is optional.\n\n```sh\ncurl -i http://localhost:8891/users/123/posts/456\n```\n\nBecause the `tag` query parameter is not provided, the response will be:\n\n```json\n{\n \"msg\": \"UserID: 123, PostID: 456, Tag: none\"\n}\n```\n\n#### Casing\n\nField names use lowerCamelCase by default. You can override this by using the `json` tag.\n\n## SumTypes\n\nGiven the following request verb:\n\n```go\n//ftl:enum export\ntype SumType interface {\n\ttag()\n}\n\ntype A string\n\nfunc (A) tag() {}\n\ntype B []string\n\nfunc (B) tag() {}\n\n//ftl:ingress http POST /typeenum\nfunc TypeEnum(ctx context.Context, req builtin.HttpRequest[SumType, ftl.Unit, ftl.Unit]) (builtin.HttpResponse[SumType, string], error) {\n\treturn builtin.HttpResponse[SumType, string]{Body: ftl.Some(req.Body)}, nil\n}\n```\n\nThe following curl request will map the `SumType` name and value to the `req.Body`:\n\n```sh\ncurl -X POST \"http://localhost:8891/typeenum\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"name\": \"A\", \"value\": \"sample\"}'\n```\n\nThe response will be:\n\n```json\n{\n \"name\": \"A\",\n \"value\": \"sample\"\n}\n```\n\n## Encoding query params as JSON\n\nComplex query params can also be encoded as JSON using the `@json` query parameter. For example:\n\n> `{\"tag\":\"ftl\"}` url-encoded is `%7B%22tag%22%3A%22ftl%22%7D`\n\n```bash\ncurl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D\n```\n\n\n\n", - "//ftl:retry": "## Retries\n\nSome FTL features allow specifying a retry policy via a Go comment directive. Retries back off exponentially until the maximum is reached.\n\nThe directive has the following syntax:\n\n\n```go\n//ftl:retry [] [] [catch ]\n```\n\n\nFor example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.\n\n\n```go\n//ftl:retry 10 5s 1m\nfunc Process(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\n### PubSub\n\nSubscribers can have a retry policy. For example:\n\n\n```go\n//ftl:subscribe exampleSubscription\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n...\n}\n```\n### FSM\n\nRetries can be declared on the FSM or on individual transition verbs. Retries declared on a verb take precedence over ones declared on the FSM. For example:\n```go\n//ftl:retry 10 1s 10s\nvar fsm = ftl.FSM(\"fsm\",\n\tftl.Start(Start),\n\tftl.Transition(Start, End),\n)\n\n//ftl:verb\n//ftl:retry 1 1s 1s\nfunc Start(ctx context.Context, in Event) error {\n\t// Start uses its own retry policy\n}\n\n\n//ftl:verb\nfunc End(ctx context.Context, in Event) error {\n\t// End inherits the default retry policy from the FSM\n}\n```\n\n\n## Catching\nAfter all retries have failed, a catch verb can be used to safely recover.\n\nThese catch verbs have a request type of `builtin.CatchRequest` and no response type. If a catch verb returns an error, it will be retried until it succeeds so it is important to handle errors carefully.\n\n\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n...\n}\n\n//ftl:verb\nfunc RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error {\n// safely handle final failure of the payment\n}\n```\n\nFor FSMs, after a catch verb has been successfully called the FSM will moved to the failed state.", - "//ftl:subscribe": "## PubSub\n\nFTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent), subscriptions (a cursor over the topic), and subscribers (functions events are delivered to). Subscribers are, as you would expect, sinks. Each subscription is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each subscription may have multiple subscribers, in which case events will be distributed among them.\n\n\nFirst, declare a new topic:\n\n```go\nvar Invoices = ftl.Topic[Invoice](\"invoices\")\n```\n\nThen declare each subscription on the topic:\n\n```go\nvar _ = ftl.Subscription(Invoices, \"emailInvoices\")\n```\n\nAnd finally define a Sink to consume from the subscription:\n\n```go\n//ftl:subscribe emailInvoices\nfunc SendInvoiceEmail(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\nEvents can be published to a topic like so:\n\n```go\nInvoices.Publish(ctx, Invoice{...})\n```\n\n> **NOTE!**\n> PubSub topics cannot be published to from outside the module that declared them, they can only be subscribed to. That is, if a topic is declared in module `A`, module `B` cannot publish to it.\n", + "//ftl:retry": "## Retries\n\nSome FTL features allow specifying a retry policy via a Go comment directive. Retries back off exponentially until the maximum is reached.\n\nThe directive has the following syntax:\n\n```go\n//ftl:retry [] [] [catch ]\n```\n\nFor example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.\n\n```go\n//ftl:retry 10 5s 1m\nfunc Process(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\n### PubSub\n\nSubscribers can have a retry policy. For example:\n```go\n//ftl:subscribe exampleSubscription\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n```\n\n### FSM\n\nRetries can be declared on the FSM or on individual transition verbs. Retries declared on a verb take precedence over ones declared on the FSM. For example:\n```go\n//ftl:retry 10 1s 10s\nvar fsm = ftl.FSM(\"fsm\",\n\tftl.Start(Start),\n\tftl.Transition(Start, End),\n)\n\n//ftl:verb\n//ftl:retry 1 1s 1s\nfunc Start(ctx context.Context, in Event) error {\n\t// Start uses its own retry policy\n}\n\n\n//ftl:verb\nfunc End(ctx context.Context, in Event) error {\n\t// End inherits the default retry policy from the FSM\n}\n```\n\n\n## Catching\nAfter all retries have failed, a catch verb can be used to safely recover.\n\nThese catch verbs have a request type of `builtin.CatchRequest` and no response type. If a catch verb returns an error, it will be retried until it succeeds so it is important to handle errors carefully.\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n\n//ftl:verb\nfunc RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error {\n // safely handle final failure of the payment\n}\n```\n\nFor FSMs, after a catch verb has been successfully called the FSM will moved to the failed state.", + "//ftl:subscribe": "## PubSub\n\nFTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent), subscriptions (a cursor over the topic), and subscribers (functions events are delivered to). Subscribers are, as you would expect, sinks. Each subscription is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each subscription may have multiple subscribers, in which case events will be distributed among them.\n\nFirst, declare a new topic:\n\n```go\nvar Invoices = ftl.Topic[Invoice](\"invoices\")\n```\n\nThen declare each subscription on the topic:\n\n```go\nvar _ = ftl.Subscription(Invoices, \"emailInvoices\")\n```\n\nAnd finally define a Sink to consume from the subscription:\n\n```go\n//ftl:subscribe emailInvoices\nfunc SendInvoiceEmail(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\nEvents can be published to a topic like so:\n\n```go\nInvoices.Publish(ctx, Invoice{...})\n```\n\n> **NOTE!**\n> PubSub topics cannot be published to from outside the module that declared them, they can only be subscribed to. That is, if a topic is declared in module `A`, module `B` cannot publish to it.\n", "//ftl:typealias": "## Type aliases\n\nA type alias is an alternate name for an existing type. It can be declared like so:\n\n```go\n//ftl:typealias\ntype Alias Target\n```\nor\n```go\n//ftl:typealias\ntype Alias = Target\n```\n\neg.\n\n```go\n//ftl:typealias\ntype UserID string\n\n//ftl:typealias\ntype UserToken = string\n```\n", "//ftl:verb": "## Verbs\n\n## Defining Verbs\n\n\nTo declare a Verb, write a normal Go function with the following signature, annotated with the Go [comment directive](https://tip.golang.org/doc/comment#syntax) `//ftl:verb`:\n\n```go\n//ftl:verb\nfunc F(context.Context, In) (Out, error) { }\n```\n\neg.\n\n```go\ntype EchoRequest struct {}\n\ntype EchoResponse struct {}\n\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest) (EchoResponse, error) {\n // ...\n}\n```\n\n\nBy default verbs are only [visible](../visibility) to other verbs in the same module.\n\n## Calling Verbs\n\n\nTo call a verb, import the module's verb client (`{ModuleName}.{VerbName}Client`), add it to your verb's signature, then invoke it as a function. eg.\n\n```go\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest, tc time.TimeClient) (EchoResponse, error) {\n\tout, err := tc(ctx, TimeRequest{...})\n}\n```\n\nVerb clients are generated by FTL. If the callee verb belongs to the same module as the caller, you must build the \nmodule first (with callee verb defined) in order to generate its client for use by the caller. Local verb clients are \navailable in the generated `types.ftl.go` file as `{VerbName}Client`.\n\n", } From ee192a637b3678eb90dca987e18890432ee49ac4 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Tue, 8 Oct 2024 17:55:53 +1000 Subject: [PATCH 23/34] whitespace --- internal/lsp/hoveritems.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/lsp/hoveritems.go b/internal/lsp/hoveritems.go index e5cbd73ef1..7ffc48fd47 100644 --- a/internal/lsp/hoveritems.go +++ b/internal/lsp/hoveritems.go @@ -2,11 +2,11 @@ package lsp var hoverMap = map[string]string{ - "//ftl:cron": "## Cron\n\nA cron job is an Empty verb that will be called on a schedule. The syntax is described [here](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/crontab.html).\n\nYou can also use a shorthand syntax for the cron job, supporting seconds (`s`), minutes (`m`), hours (`h`), and specific days of the week (e.g. `Mon`).\n\n### Examples\n\nThe following function will be called hourly:\n\n```go\n//ftl:cron 0 * * * *\nfunc Hourly(ctx context.Context) error {\n // ...\n}\n```\nEvery 12 hours, starting at UTC midnight:\n\n```go\n//ftl:cron 12h\nfunc TwiceADay(ctx context.Context) error {\n // ...\n}\n```\n\nEvery Monday at UTC midnight:\n\n```go\n//ftl:cron Mon\nfunc Mondays(ctx context.Context) error {\n // ...\n}\n```", - "//ftl:enum": "## Type enums (sum types)\n\n[Sum types](https://en.wikipedia.org/wiki/Tagged_union) are supported by FTL's type system, but aren't directly supported by Go. However they can be approximated with the use of [sealed interfaces](https://blog.chewxy.com/2018/03/18/golang-interfaces/). To declare a sum type in FTL use the comment directive `//ftl:enum`:\n\n```go\n//ftl:enum\ntype Animal interface { animal() }\n\ntype Cat struct {}\nfunc (Cat) animal() {}\n\ntype Dog struct {}\nfunc (Dog) animal() {}\n```\n## Value enums\n\nA value enum is an enumerated set of string or integer values.\n\n```go\n//ftl:enum\ntype Colour string\n\nconst (\n Red Colour = \"red\"\n Green Colour = \"green\"\n Blue Colour = \"blue\"\n)\n```\n", - "//ftl:ingress": "## HTTP Ingress\n\nVerbs annotated with `ftl:ingress` will be exposed via HTTP (`http` is the default ingress type). These endpoints will then be available on one of our default `ingress` ports (local development defaults to `http://localhost:8891`).\n\nThe following will be available at `http://localhost:8891/http/users/123/posts?postId=456`.\n\n\n```go\ntype GetRequestPathParams struct {\n\tUserID string `json:\"userId\"`\n}\n\ntype GetRequestQueryParams struct {\n\tPostID string `json:\"postId\"`\n}\n\ntype GetResponse struct {\n\tMessage string `json:\"msg\"`\n}\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, GetRequestPathParams, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\nBecause the example above only has a single path parameter it can be simplified by just using a scalar such as `string` or `int64` as the path parameter type:\n\n```go\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, int64, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\n> **NOTE!**\n> The `req` and `resp` types of HTTP `ingress` [verbs](../verbs) must be `builtin.HttpRequest` and `builtin.HttpResponse` respectively. These types provide the necessary fields for HTTP `ingress` (`headers`, `statusCode`, etc.)\n>\n> You will need to import `ftl/builtin`.\n\nKey points:\n\n- `ingress` verbs will be automatically exported by default.\n\n## Field mapping\n\nThe `HttpRequest` request object takes 3 type parameters, the body, the path parameters and the query parameters.\n\nGiven the following request verb:\n\n```go\n\ntype PostBody struct{\n\tTitle string `json:\"title\"`\n\tContent string `json:\"content\"`\n\tTag ftl.Option[string] `json:\"tag\"`\n}\ntype PostPathParams struct {\n\tUserID string `json:\"userId\"`\n\tPostID string `json:\"postId\"`\n}\n\ntype PostQueryParams struct {\n\tPublish boolean `json:\"publish\"`\n}\n\n//ftl:ingress http PUT /users/{userId}/posts/{postId}\nfunc Get(ctx context.Context, req builtin.HttpRequest[PostBody, PostPathParams, PostQueryParams]) (builtin.HttpResponse[GetResponse, string], error) {\n\treturn builtin.HttpResponse[GetResponse, string]{\n\t\tHeaders: map[string][]string{\"Get\": {\"Header from FTL\"}},\n\t\tBody: ftl.Some(GetResponse{\n\t\t\tMessage: fmt.Sprintf(\"UserID: %s, PostID: %s, Tag: %s\", req.pathParameters.UserID, req.pathParameters.PostID, req.Body.Tag.Default(\"none\")),\n\t\t}),\n\t}, nil\n}\n```\n\nThe rules for how each element is mapped are slightly different, as they have a different structure:\n\n- The body is mapped directly to the body of the request, generally as a JSON object. Scalars are also supported, as well as []byte to get the raw body. If they type is `any` then it will be assumed to be JSON and mapped to the appropriate types based on the JSON structure.\n- The path parameters can be mapped directly to an object with field names corresponding to the name of the path parameter. If there is only a single path parameter it can be injected directly as a scalar. They can also be injected as a `map[string]string`.\n- The path parameters can also be mapped directly to an object with field names corresponding to the name of the path parameter. They can also be injected directly as a `map[string]string`, or `map[string][]string` for multiple values.\n\n#### Optional fields\n\nOptional fields are represented by the `ftl.Option` type. The `Option` type is a wrapper around the actual type and can be `Some` or `None`. In the example above, the `Tag` field is optional.\n\n```sh\ncurl -i http://localhost:8891/users/123/posts/456\n```\n\nBecause the `tag` query parameter is not provided, the response will be:\n\n```json\n{\n \"msg\": \"UserID: 123, PostID: 456, Tag: none\"\n}\n```\n\n#### Casing\n\nField names use lowerCamelCase by default. You can override this by using the `json` tag.\n\n## SumTypes\n\nGiven the following request verb:\n\n```go\n//ftl:enum export\ntype SumType interface {\n\ttag()\n}\n\ntype A string\n\nfunc (A) tag() {}\n\ntype B []string\n\nfunc (B) tag() {}\n\n//ftl:ingress http POST /typeenum\nfunc TypeEnum(ctx context.Context, req builtin.HttpRequest[SumType, ftl.Unit, ftl.Unit]) (builtin.HttpResponse[SumType, string], error) {\n\treturn builtin.HttpResponse[SumType, string]{Body: ftl.Some(req.Body)}, nil\n}\n```\n\nThe following curl request will map the `SumType` name and value to the `req.Body`:\n\n```sh\ncurl -X POST \"http://localhost:8891/typeenum\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"name\": \"A\", \"value\": \"sample\"}'\n```\n\nThe response will be:\n\n```json\n{\n \"name\": \"A\",\n \"value\": \"sample\"\n}\n```\n\n## Encoding query params as JSON\n\nComplex query params can also be encoded as JSON using the `@json` query parameter. For example:\n\n> `{\"tag\":\"ftl\"}` url-encoded is `%7B%22tag%22%3A%22ftl%22%7D`\n\n```bash\ncurl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D\n```\n\n\n\n", - "//ftl:retry": "## Retries\n\nSome FTL features allow specifying a retry policy via a Go comment directive. Retries back off exponentially until the maximum is reached.\n\nThe directive has the following syntax:\n\n```go\n//ftl:retry [] [] [catch ]\n```\n\nFor example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.\n\n```go\n//ftl:retry 10 5s 1m\nfunc Process(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\n### PubSub\n\nSubscribers can have a retry policy. For example:\n```go\n//ftl:subscribe exampleSubscription\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n```\n\n### FSM\n\nRetries can be declared on the FSM or on individual transition verbs. Retries declared on a verb take precedence over ones declared on the FSM. For example:\n```go\n//ftl:retry 10 1s 10s\nvar fsm = ftl.FSM(\"fsm\",\n\tftl.Start(Start),\n\tftl.Transition(Start, End),\n)\n\n//ftl:verb\n//ftl:retry 1 1s 1s\nfunc Start(ctx context.Context, in Event) error {\n\t// Start uses its own retry policy\n}\n\n\n//ftl:verb\nfunc End(ctx context.Context, in Event) error {\n\t// End inherits the default retry policy from the FSM\n}\n```\n\n\n## Catching\nAfter all retries have failed, a catch verb can be used to safely recover.\n\nThese catch verbs have a request type of `builtin.CatchRequest` and no response type. If a catch verb returns an error, it will be retried until it succeeds so it is important to handle errors carefully.\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n\n//ftl:verb\nfunc RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error {\n // safely handle final failure of the payment\n}\n```\n\nFor FSMs, after a catch verb has been successfully called the FSM will moved to the failed state.", + "//ftl:cron": "## Cron\n\nA cron job is an Empty verb that will be called on a schedule. The syntax is described [here](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/crontab.html).\n\nYou can also use a shorthand syntax for the cron job, supporting seconds (`s`), minutes (`m`), hours (`h`), and specific days of the week (e.g. `Mon`).\n\n### Examples\n\nThe following function will be called hourly:\n\n```go\n//ftl:cron 0 * * * *\nfunc Hourly(ctx context.Context) error {\n // ...\n}\n```\nEvery 12 hours, starting at UTC midnight:\n\n```go\n//ftl:cron 12h\nfunc TwiceADay(ctx context.Context) error {\n // ...\n}\n```\n\nEvery Monday at UTC midnight:\n\n```go\n//ftl:cron Mon\nfunc Mondays(ctx context.Context) error {\n // ...\n}\n```", + "//ftl:enum": "## Type enums (sum types)\n\n[Sum types](https://en.wikipedia.org/wiki/Tagged_union) are supported by FTL's type system, but aren't directly supported by Go. However they can be approximated with the use of [sealed interfaces](https://blog.chewxy.com/2018/03/18/golang-interfaces/). To declare a sum type in FTL use the comment directive `//ftl:enum`:\n\n```go\n//ftl:enum\ntype Animal interface { animal() }\n\ntype Cat struct {}\nfunc (Cat) animal() {}\n\ntype Dog struct {}\nfunc (Dog) animal() {}\n```\n## Value enums\n\nA value enum is an enumerated set of string or integer values.\n\n```go\n//ftl:enum\ntype Colour string\n\nconst (\n Red Colour = \"red\"\n Green Colour = \"green\"\n Blue Colour = \"blue\"\n)\n```\n", + "//ftl:ingress": "## HTTP Ingress\n\nVerbs annotated with `ftl:ingress` will be exposed via HTTP (`http` is the default ingress type). These endpoints will then be available on one of our default `ingress` ports (local development defaults to `http://localhost:8891`).\n\nThe following will be available at `http://localhost:8891/http/users/123/posts?postId=456`.\n\n\n```go\ntype GetRequestPathParams struct {\n\tUserID string `json:\"userId\"`\n}\n\ntype GetRequestQueryParams struct {\n\tPostID string `json:\"postId\"`\n}\n\ntype GetResponse struct {\n\tMessage string `json:\"msg\"`\n}\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, GetRequestPathParams, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\nBecause the example above only has a single path parameter it can be simplified by just using a scalar such as `string` or `int64` as the path parameter type:\n\n```go\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, int64, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\n> **NOTE!**\n> The `req` and `resp` types of HTTP `ingress` [verbs](../verbs) must be `builtin.HttpRequest` and `builtin.HttpResponse` respectively. These types provide the necessary fields for HTTP `ingress` (`headers`, `statusCode`, etc.)\n>\n> You will need to import `ftl/builtin`.\n\nKey points:\n\n- `ingress` verbs will be automatically exported by default.\n\n## Field mapping\n\nThe `HttpRequest` request object takes 3 type parameters, the body, the path parameters and the query parameters.\n\nGiven the following request verb:\n\n```go\n\ntype PostBody struct{\n\tTitle string `json:\"title\"`\n\tContent string `json:\"content\"`\n\tTag ftl.Option[string] `json:\"tag\"`\n}\ntype PostPathParams struct {\n\tUserID string `json:\"userId\"`\n\tPostID string `json:\"postId\"`\n}\n\ntype PostQueryParams struct {\n\tPublish boolean `json:\"publish\"`\n}\n\n//ftl:ingress http PUT /users/{userId}/posts/{postId}\nfunc Get(ctx context.Context, req builtin.HttpRequest[PostBody, PostPathParams, PostQueryParams]) (builtin.HttpResponse[GetResponse, string], error) {\n\treturn builtin.HttpResponse[GetResponse, string]{\n\t\tHeaders: map[string][]string{\"Get\": {\"Header from FTL\"}},\n\t\tBody: ftl.Some(GetResponse{\n\t\t\tMessage: fmt.Sprintf(\"UserID: %s, PostID: %s, Tag: %s\", req.pathParameters.UserID, req.pathParameters.PostID, req.Body.Tag.Default(\"none\")),\n\t\t}),\n\t}, nil\n}\n```\n\nThe rules for how each element is mapped are slightly different, as they have a different structure:\n\n- The body is mapped directly to the body of the request, generally as a JSON object. Scalars are also supported, as well as []byte to get the raw body. If they type is `any` then it will be assumed to be JSON and mapped to the appropriate types based on the JSON structure.\n- The path parameters can be mapped directly to an object with field names corresponding to the name of the path parameter. If there is only a single path parameter it can be injected directly as a scalar. They can also be injected as a `map[string]string`.\n- The path parameters can also be mapped directly to an object with field names corresponding to the name of the path parameter. They can also be injected directly as a `map[string]string`, or `map[string][]string` for multiple values.\n\n#### Optional fields\n\nOptional fields are represented by the `ftl.Option` type. The `Option` type is a wrapper around the actual type and can be `Some` or `None`. In the example above, the `Tag` field is optional.\n\n```sh\ncurl -i http://localhost:8891/users/123/posts/456\n```\n\nBecause the `tag` query parameter is not provided, the response will be:\n\n```json\n{\n \"msg\": \"UserID: 123, PostID: 456, Tag: none\"\n}\n```\n\n#### Casing\n\nField names use lowerCamelCase by default. You can override this by using the `json` tag.\n\n## SumTypes\n\nGiven the following request verb:\n\n```go\n//ftl:enum export\ntype SumType interface {\n\ttag()\n}\n\ntype A string\n\nfunc (A) tag() {}\n\ntype B []string\n\nfunc (B) tag() {}\n\n//ftl:ingress http POST /typeenum\nfunc TypeEnum(ctx context.Context, req builtin.HttpRequest[SumType, ftl.Unit, ftl.Unit]) (builtin.HttpResponse[SumType, string], error) {\n\treturn builtin.HttpResponse[SumType, string]{Body: ftl.Some(req.Body)}, nil\n}\n```\n\nThe following curl request will map the `SumType` name and value to the `req.Body`:\n\n```sh\ncurl -X POST \"http://localhost:8891/typeenum\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"name\": \"A\", \"value\": \"sample\"}'\n```\n\nThe response will be:\n\n```json\n{\n \"name\": \"A\",\n \"value\": \"sample\"\n}\n```\n\n## Encoding query params as JSON\n\nComplex query params can also be encoded as JSON using the `@json` query parameter. For example:\n\n> `{\"tag\":\"ftl\"}` url-encoded is `%7B%22tag%22%3A%22ftl%22%7D`\n\n```bash\ncurl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D\n```\n\n\n\n", + "//ftl:retry": "## Retries\n\nSome FTL features allow specifying a retry policy via a Go comment directive. Retries back off exponentially until the maximum is reached.\n\nThe directive has the following syntax:\n\n```go\n//ftl:retry [] [] [catch ]\n```\n\nFor example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.\n\n```go\n//ftl:retry 10 5s 1m\nfunc Process(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\n### PubSub\n\nSubscribers can have a retry policy. For example:\n```go\n//ftl:subscribe exampleSubscription\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n```\n\n### FSM\n\nRetries can be declared on the FSM or on individual transition verbs. Retries declared on a verb take precedence over ones declared on the FSM. For example:\n```go\n//ftl:retry 10 1s 10s\nvar fsm = ftl.FSM(\"fsm\",\n\tftl.Start(Start),\n\tftl.Transition(Start, End),\n)\n\n//ftl:verb\n//ftl:retry 1 1s 1s\nfunc Start(ctx context.Context, in Event) error {\n\t// Start uses its own retry policy\n}\n\n\n//ftl:verb\nfunc End(ctx context.Context, in Event) error {\n\t// End inherits the default retry policy from the FSM\n}\n```\n\n\n## Catching\nAfter all retries have failed, a catch verb can be used to safely recover.\n\nThese catch verbs have a request type of `builtin.CatchRequest` and no response type. If a catch verb returns an error, it will be retried until it succeeds so it is important to handle errors carefully.\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n\n//ftl:verb\nfunc RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error {\n // safely handle final failure of the payment\n}\n```\n\nFor FSMs, after a catch verb has been successfully called the FSM will moved to the failed state.", "//ftl:subscribe": "## PubSub\n\nFTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent), subscriptions (a cursor over the topic), and subscribers (functions events are delivered to). Subscribers are, as you would expect, sinks. Each subscription is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each subscription may have multiple subscribers, in which case events will be distributed among them.\n\nFirst, declare a new topic:\n\n```go\nvar Invoices = ftl.Topic[Invoice](\"invoices\")\n```\n\nThen declare each subscription on the topic:\n\n```go\nvar _ = ftl.Subscription(Invoices, \"emailInvoices\")\n```\n\nAnd finally define a Sink to consume from the subscription:\n\n```go\n//ftl:subscribe emailInvoices\nfunc SendInvoiceEmail(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\nEvents can be published to a topic like so:\n\n```go\nInvoices.Publish(ctx, Invoice{...})\n```\n\n> **NOTE!**\n> PubSub topics cannot be published to from outside the module that declared them, they can only be subscribed to. That is, if a topic is declared in module `A`, module `B` cannot publish to it.\n", "//ftl:typealias": "## Type aliases\n\nA type alias is an alternate name for an existing type. It can be declared like so:\n\n```go\n//ftl:typealias\ntype Alias Target\n```\nor\n```go\n//ftl:typealias\ntype Alias = Target\n```\n\neg.\n\n```go\n//ftl:typealias\ntype UserID string\n\n//ftl:typealias\ntype UserToken = string\n```\n", - "//ftl:verb": "## Verbs\n\n## Defining Verbs\n\n\nTo declare a Verb, write a normal Go function with the following signature, annotated with the Go [comment directive](https://tip.golang.org/doc/comment#syntax) `//ftl:verb`:\n\n```go\n//ftl:verb\nfunc F(context.Context, In) (Out, error) { }\n```\n\neg.\n\n```go\ntype EchoRequest struct {}\n\ntype EchoResponse struct {}\n\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest) (EchoResponse, error) {\n // ...\n}\n```\n\n\nBy default verbs are only [visible](../visibility) to other verbs in the same module.\n\n## Calling Verbs\n\n\nTo call a verb, import the module's verb client (`{ModuleName}.{VerbName}Client`), add it to your verb's signature, then invoke it as a function. eg.\n\n```go\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest, tc time.TimeClient) (EchoResponse, error) {\n\tout, err := tc(ctx, TimeRequest{...})\n}\n```\n\nVerb clients are generated by FTL. If the callee verb belongs to the same module as the caller, you must build the \nmodule first (with callee verb defined) in order to generate its client for use by the caller. Local verb clients are \navailable in the generated `types.ftl.go` file as `{VerbName}Client`.\n\n", + "//ftl:verb": "## Verbs\n\n## Defining Verbs\n\n\nTo declare a Verb, write a normal Go function with the following signature, annotated with the Go [comment directive](https://tip.golang.org/doc/comment#syntax) `//ftl:verb`:\n\n```go\n//ftl:verb\nfunc F(context.Context, In) (Out, error) { }\n```\n\neg.\n\n```go\ntype EchoRequest struct {}\n\ntype EchoResponse struct {}\n\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest) (EchoResponse, error) {\n // ...\n}\n```\n\n\nBy default verbs are only [visible](../visibility) to other verbs in the same module.\n\n## Calling Verbs\n\n\nTo call a verb, import the module's verb client (`{ModuleName}.{VerbName}Client`), add it to your verb's signature, then invoke it as a function. eg.\n\n```go\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest, tc time.TimeClient) (EchoResponse, error) {\n\tout, err := tc(ctx, TimeRequest{...})\n}\n```\n\nVerb clients are generated by FTL. If the callee verb belongs to the same module as the caller, you must build the \nmodule first (with callee verb defined) in order to generate its client for use by the caller. Local verb clients are \navailable in the generated `types.ftl.go` file as `{VerbName}Client`.\n\n", } From 4ea9f38a960e5151e63374109fbcfd63d9c394c1 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Tue, 8 Oct 2024 18:02:40 +1000 Subject: [PATCH 24/34] whitespace --- internal/lsp/hoveritems.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/lsp/hoveritems.go b/internal/lsp/hoveritems.go index 7ffc48fd47..c8fd2ac41e 100644 --- a/internal/lsp/hoveritems.go +++ b/internal/lsp/hoveritems.go @@ -5,8 +5,8 @@ var hoverMap = map[string]string{ "//ftl:cron": "## Cron\n\nA cron job is an Empty verb that will be called on a schedule. The syntax is described [here](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/crontab.html).\n\nYou can also use a shorthand syntax for the cron job, supporting seconds (`s`), minutes (`m`), hours (`h`), and specific days of the week (e.g. `Mon`).\n\n### Examples\n\nThe following function will be called hourly:\n\n```go\n//ftl:cron 0 * * * *\nfunc Hourly(ctx context.Context) error {\n // ...\n}\n```\nEvery 12 hours, starting at UTC midnight:\n\n```go\n//ftl:cron 12h\nfunc TwiceADay(ctx context.Context) error {\n // ...\n}\n```\n\nEvery Monday at UTC midnight:\n\n```go\n//ftl:cron Mon\nfunc Mondays(ctx context.Context) error {\n // ...\n}\n```", "//ftl:enum": "## Type enums (sum types)\n\n[Sum types](https://en.wikipedia.org/wiki/Tagged_union) are supported by FTL's type system, but aren't directly supported by Go. However they can be approximated with the use of [sealed interfaces](https://blog.chewxy.com/2018/03/18/golang-interfaces/). To declare a sum type in FTL use the comment directive `//ftl:enum`:\n\n```go\n//ftl:enum\ntype Animal interface { animal() }\n\ntype Cat struct {}\nfunc (Cat) animal() {}\n\ntype Dog struct {}\nfunc (Dog) animal() {}\n```\n## Value enums\n\nA value enum is an enumerated set of string or integer values.\n\n```go\n//ftl:enum\ntype Colour string\n\nconst (\n Red Colour = \"red\"\n Green Colour = \"green\"\n Blue Colour = \"blue\"\n)\n```\n", "//ftl:ingress": "## HTTP Ingress\n\nVerbs annotated with `ftl:ingress` will be exposed via HTTP (`http` is the default ingress type). These endpoints will then be available on one of our default `ingress` ports (local development defaults to `http://localhost:8891`).\n\nThe following will be available at `http://localhost:8891/http/users/123/posts?postId=456`.\n\n\n```go\ntype GetRequestPathParams struct {\n\tUserID string `json:\"userId\"`\n}\n\ntype GetRequestQueryParams struct {\n\tPostID string `json:\"postId\"`\n}\n\ntype GetResponse struct {\n\tMessage string `json:\"msg\"`\n}\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, GetRequestPathParams, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\nBecause the example above only has a single path parameter it can be simplified by just using a scalar such as `string` or `int64` as the path parameter type:\n\n```go\n\n//ftl:ingress GET /http/users/{userId}/posts\nfunc Get(ctx context.Context, req builtin.HttpRequest[ftl.Unit, int64, GetRequestQueryParams]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {\n // ...\n}\n```\n\n> **NOTE!**\n> The `req` and `resp` types of HTTP `ingress` [verbs](../verbs) must be `builtin.HttpRequest` and `builtin.HttpResponse` respectively. These types provide the necessary fields for HTTP `ingress` (`headers`, `statusCode`, etc.)\n>\n> You will need to import `ftl/builtin`.\n\nKey points:\n\n- `ingress` verbs will be automatically exported by default.\n\n## Field mapping\n\nThe `HttpRequest` request object takes 3 type parameters, the body, the path parameters and the query parameters.\n\nGiven the following request verb:\n\n```go\n\ntype PostBody struct{\n\tTitle string `json:\"title\"`\n\tContent string `json:\"content\"`\n\tTag ftl.Option[string] `json:\"tag\"`\n}\ntype PostPathParams struct {\n\tUserID string `json:\"userId\"`\n\tPostID string `json:\"postId\"`\n}\n\ntype PostQueryParams struct {\n\tPublish boolean `json:\"publish\"`\n}\n\n//ftl:ingress http PUT /users/{userId}/posts/{postId}\nfunc Get(ctx context.Context, req builtin.HttpRequest[PostBody, PostPathParams, PostQueryParams]) (builtin.HttpResponse[GetResponse, string], error) {\n\treturn builtin.HttpResponse[GetResponse, string]{\n\t\tHeaders: map[string][]string{\"Get\": {\"Header from FTL\"}},\n\t\tBody: ftl.Some(GetResponse{\n\t\t\tMessage: fmt.Sprintf(\"UserID: %s, PostID: %s, Tag: %s\", req.pathParameters.UserID, req.pathParameters.PostID, req.Body.Tag.Default(\"none\")),\n\t\t}),\n\t}, nil\n}\n```\n\nThe rules for how each element is mapped are slightly different, as they have a different structure:\n\n- The body is mapped directly to the body of the request, generally as a JSON object. Scalars are also supported, as well as []byte to get the raw body. If they type is `any` then it will be assumed to be JSON and mapped to the appropriate types based on the JSON structure.\n- The path parameters can be mapped directly to an object with field names corresponding to the name of the path parameter. If there is only a single path parameter it can be injected directly as a scalar. They can also be injected as a `map[string]string`.\n- The path parameters can also be mapped directly to an object with field names corresponding to the name of the path parameter. They can also be injected directly as a `map[string]string`, or `map[string][]string` for multiple values.\n\n#### Optional fields\n\nOptional fields are represented by the `ftl.Option` type. The `Option` type is a wrapper around the actual type and can be `Some` or `None`. In the example above, the `Tag` field is optional.\n\n```sh\ncurl -i http://localhost:8891/users/123/posts/456\n```\n\nBecause the `tag` query parameter is not provided, the response will be:\n\n```json\n{\n \"msg\": \"UserID: 123, PostID: 456, Tag: none\"\n}\n```\n\n#### Casing\n\nField names use lowerCamelCase by default. You can override this by using the `json` tag.\n\n## SumTypes\n\nGiven the following request verb:\n\n```go\n//ftl:enum export\ntype SumType interface {\n\ttag()\n}\n\ntype A string\n\nfunc (A) tag() {}\n\ntype B []string\n\nfunc (B) tag() {}\n\n//ftl:ingress http POST /typeenum\nfunc TypeEnum(ctx context.Context, req builtin.HttpRequest[SumType, ftl.Unit, ftl.Unit]) (builtin.HttpResponse[SumType, string], error) {\n\treturn builtin.HttpResponse[SumType, string]{Body: ftl.Some(req.Body)}, nil\n}\n```\n\nThe following curl request will map the `SumType` name and value to the `req.Body`:\n\n```sh\ncurl -X POST \"http://localhost:8891/typeenum\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"name\": \"A\", \"value\": \"sample\"}'\n```\n\nThe response will be:\n\n```json\n{\n \"name\": \"A\",\n \"value\": \"sample\"\n}\n```\n\n## Encoding query params as JSON\n\nComplex query params can also be encoded as JSON using the `@json` query parameter. For example:\n\n> `{\"tag\":\"ftl\"}` url-encoded is `%7B%22tag%22%3A%22ftl%22%7D`\n\n```bash\ncurl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D\n```\n\n\n\n", - "//ftl:retry": "## Retries\n\nSome FTL features allow specifying a retry policy via a Go comment directive. Retries back off exponentially until the maximum is reached.\n\nThe directive has the following syntax:\n\n```go\n//ftl:retry [] [] [catch ]\n```\n\nFor example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.\n\n```go\n//ftl:retry 10 5s 1m\nfunc Process(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\n### PubSub\n\nSubscribers can have a retry policy. For example:\n```go\n//ftl:subscribe exampleSubscription\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n```\n\n### FSM\n\nRetries can be declared on the FSM or on individual transition verbs. Retries declared on a verb take precedence over ones declared on the FSM. For example:\n```go\n//ftl:retry 10 1s 10s\nvar fsm = ftl.FSM(\"fsm\",\n\tftl.Start(Start),\n\tftl.Transition(Start, End),\n)\n\n//ftl:verb\n//ftl:retry 1 1s 1s\nfunc Start(ctx context.Context, in Event) error {\n\t// Start uses its own retry policy\n}\n\n\n//ftl:verb\nfunc End(ctx context.Context, in Event) error {\n\t// End inherits the default retry policy from the FSM\n}\n```\n\n\n## Catching\nAfter all retries have failed, a catch verb can be used to safely recover.\n\nThese catch verbs have a request type of `builtin.CatchRequest` and no response type. If a catch verb returns an error, it will be retried until it succeeds so it is important to handle errors carefully.\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n ...\n}\n\n//ftl:verb\nfunc RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error {\n // safely handle final failure of the payment\n}\n```\n\nFor FSMs, after a catch verb has been successfully called the FSM will moved to the failed state.", - "//ftl:subscribe": "## PubSub\n\nFTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent), subscriptions (a cursor over the topic), and subscribers (functions events are delivered to). Subscribers are, as you would expect, sinks. Each subscription is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each subscription may have multiple subscribers, in which case events will be distributed among them.\n\nFirst, declare a new topic:\n\n```go\nvar Invoices = ftl.Topic[Invoice](\"invoices\")\n```\n\nThen declare each subscription on the topic:\n\n```go\nvar _ = ftl.Subscription(Invoices, \"emailInvoices\")\n```\n\nAnd finally define a Sink to consume from the subscription:\n\n```go\n//ftl:subscribe emailInvoices\nfunc SendInvoiceEmail(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\nEvents can be published to a topic like so:\n\n```go\nInvoices.Publish(ctx, Invoice{...})\n```\n\n> **NOTE!**\n> PubSub topics cannot be published to from outside the module that declared them, they can only be subscribed to. That is, if a topic is declared in module `A`, module `B` cannot publish to it.\n", + "//ftl:retry": "## Retries\n\nSome FTL features allow specifying a retry policy via a Go comment directive. Retries back off exponentially until the maximum is reached.\n\nThe directive has the following syntax:\n\n\n```go\n//ftl:retry [] [] [catch ]\n```\n\n\nFor example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.\n\n\n```go\n//ftl:retry 10 5s 1m\nfunc Process(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\n### PubSub\n\nSubscribers can have a retry policy. For example:\n\n\n```go\n//ftl:subscribe exampleSubscription\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n...\n}\n```\n### FSM\n\nRetries can be declared on the FSM or on individual transition verbs. Retries declared on a verb take precedence over ones declared on the FSM. For example:\n```go\n//ftl:retry 10 1s 10s\nvar fsm = ftl.FSM(\"fsm\",\n\tftl.Start(Start),\n\tftl.Transition(Start, End),\n)\n\n//ftl:verb\n//ftl:retry 1 1s 1s\nfunc Start(ctx context.Context, in Event) error {\n\t// Start uses its own retry policy\n}\n\n\n//ftl:verb\nfunc End(ctx context.Context, in Event) error {\n\t// End inherits the default retry policy from the FSM\n}\n```\n\n\n## Catching\nAfter all retries have failed, a catch verb can be used to safely recover.\n\nThese catch verbs have a request type of `builtin.CatchRequest` and no response type. If a catch verb returns an error, it will be retried until it succeeds so it is important to handle errors carefully.\n\n\n\n```go\n//ftl:retry 5 1s catch recoverPaymentProcessing\nfunc ProcessPayment(ctx context.Context, payment Payment) error {\n...\n}\n\n//ftl:verb\nfunc RecoverPaymentProcessing(ctx context.Context, request builtin.CatchRequest[Payment]) error {\n// safely handle final failure of the payment\n}\n```\n\nFor FSMs, after a catch verb has been successfully called the FSM will moved to the failed state.", + "//ftl:subscribe": "## PubSub\n\nFTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent), subscriptions (a cursor over the topic), and subscribers (functions events are delivered to). Subscribers are, as you would expect, sinks. Each subscription is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each subscription may have multiple subscribers, in which case events will be distributed among them.\n\n\nFirst, declare a new topic:\n\n```go\nvar Invoices = ftl.Topic[Invoice](\"invoices\")\n```\n\nThen declare each subscription on the topic:\n\n```go\nvar _ = ftl.Subscription(Invoices, \"emailInvoices\")\n```\n\nAnd finally define a Sink to consume from the subscription:\n\n```go\n//ftl:subscribe emailInvoices\nfunc SendInvoiceEmail(ctx context.Context, in Invoice) error {\n // ...\n}\n```\n\nEvents can be published to a topic like so:\n\n```go\nInvoices.Publish(ctx, Invoice{...})\n```\n\n> **NOTE!**\n> PubSub topics cannot be published to from outside the module that declared them, they can only be subscribed to. That is, if a topic is declared in module `A`, module `B` cannot publish to it.\n", "//ftl:typealias": "## Type aliases\n\nA type alias is an alternate name for an existing type. It can be declared like so:\n\n```go\n//ftl:typealias\ntype Alias Target\n```\nor\n```go\n//ftl:typealias\ntype Alias = Target\n```\n\neg.\n\n```go\n//ftl:typealias\ntype UserID string\n\n//ftl:typealias\ntype UserToken = string\n```\n", "//ftl:verb": "## Verbs\n\n## Defining Verbs\n\n\nTo declare a Verb, write a normal Go function with the following signature, annotated with the Go [comment directive](https://tip.golang.org/doc/comment#syntax) `//ftl:verb`:\n\n```go\n//ftl:verb\nfunc F(context.Context, In) (Out, error) { }\n```\n\neg.\n\n```go\ntype EchoRequest struct {}\n\ntype EchoResponse struct {}\n\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest) (EchoResponse, error) {\n // ...\n}\n```\n\n\nBy default verbs are only [visible](../visibility) to other verbs in the same module.\n\n## Calling Verbs\n\n\nTo call a verb, import the module's verb client (`{ModuleName}.{VerbName}Client`), add it to your verb's signature, then invoke it as a function. eg.\n\n```go\n//ftl:verb\nfunc Echo(ctx context.Context, in EchoRequest, tc time.TimeClient) (EchoResponse, error) {\n\tout, err := tc(ctx, TimeRequest{...})\n}\n```\n\nVerb clients are generated by FTL. If the callee verb belongs to the same module as the caller, you must build the \nmodule first (with callee verb defined) in order to generate its client for use by the caller. Local verb clients are \navailable in the generated `types.ftl.go` file as `{VerbName}Client`.\n\n", } From 65c423301c566b72b314573f46fee2c35fcea982 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 13:10:33 +1000 Subject: [PATCH 25/34] fix typealias exceptions --- .../main/java/xyz/block/ftl/deployment/VerbProcessor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java index a8f32d5ebb..9b3480a0c5 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import jakarta.inject.Singleton; @@ -50,7 +51,7 @@ VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProduce ModuleNameBuildItem moduleNameBuildItem, LaunchModeBuildItem launchModeBuildItem) { var clientDefinitions = index.getComputingIndex().getAnnotations(VerbClientDefinition.class); - log.info("Processing {} verb clients into build items", clientDefinitions.size()); + log.info("Processing {} verb clients", clientDefinitions.size()); Map clients = new HashMap<>(); for (var clientDefinition : clientDefinitions) { var iface = clientDefinition.target().asClass(); @@ -231,7 +232,9 @@ VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProduce @BuildStep public void verbsAndCron(CombinedIndexBuildItem index, BuildProducer additionalBeanBuildItem, - BuildProducer schemaContributorBuildItemBuildProducer) { + BuildProducer schemaContributorBuildItemBuildProducer, + List typeAliasBuildItems // included to force typealias processing before this + ) { Collection verbAnnotations = index.getIndex().getAnnotations(FTLDotNames.VERB); log.info("Processing {} verb annotations into decls", verbAnnotations.size()); var beans = AdditionalBeanBuildItem.builder().setUnremovable(); From a3395bd762133187f5c4086b8552c5b048abea3f Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 13:10:49 +1000 Subject: [PATCH 26/34] fix accidental whitespace --- .../block/ftl/test/TestInvokeGoFromJava.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index b5fb8c4bf4..26bcfb4361 100644 --- a/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/javaclient/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -138,7 +138,7 @@ public boolean boolVerb(boolean val, BoolVerbClient client) { @Export @Verb public @NotNull ParameterizedType parameterizedObjectVerb(@NotNull ParameterizedType val, - ParameterizedObjectVerbClient client) { + ParameterizedObjectVerbClient client) { return client.call(val); } @@ -157,7 +157,7 @@ public boolean boolVerb(boolean val, BoolVerbClient client) { @Export @Verb public @NotNull TestObjectOptionalFields testObjectOptionalFieldsVerb(@NotNull TestObjectOptionalFields val, - TestObjectOptionalFieldsVerbClient client) { + TestObjectOptionalFieldsVerbClient client) { return client.call(val); } @@ -202,7 +202,7 @@ public Boolean optionalBoolVerb(Boolean val, OptionalBoolVerbClient client) { @Export @Verb public @Nullable Map optionalStringMapVerb(@Nullable Map val, - OptionalStringMapVerbClient client) { + OptionalStringMapVerbClient client) { return client.call(val); } @@ -221,7 +221,7 @@ public Boolean optionalBoolVerb(Boolean val, OptionalBoolVerbClient client) { @Export @Verb public TestObjectOptionalFields optionalTestObjectOptionalFieldsVerb(TestObjectOptionalFields val, - OptionalTestObjectOptionalFieldsVerbClient client) { + OptionalTestObjectOptionalFieldsVerbClient client) { return client.call(val); } @@ -277,9 +277,9 @@ public TypeEnumWrapper typeWrapperEnumVerb(TypeEnumWrapper value, TypeWrapperEnu } } -// @Export -// @Verb -// public Mixed mixedEnumVerb(Mixed mixed, MixedEnumVerbClient client) { -// return client.call(mixed); -// } + // @Export + // @Verb + // public Mixed mixedEnumVerb(Mixed mixed, MixedEnumVerbClient client) { + // return client.call(mixed); + // } } From f7d1bbace34cd69e5a5a23591ad735e9ada826fc Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 13:11:22 +1000 Subject: [PATCH 27/34] refactor EnumProcessor for clarity --- .../block/ftl/deployment/EnumProcessor.java | 141 +++++++++++------- 1 file changed, 83 insertions(+), 58 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index 3939ceb40c..332c83888d 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -9,10 +9,12 @@ 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; @@ -48,63 +50,8 @@ SchemaContributorBuildItem handleEnums(CombinedIndexBuildItem index, FTLRecorder return new SchemaContributorBuildItem(new Consumer() { @Override public void accept(ModuleBuilder moduleBuilder) { - List decls = new ArrayList<>(); try { - 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); - Enum.Builder enumBuilder = Enum.newBuilder() - .setName(classInfo.simpleName()) - .setExport(exported); - if (classInfo.isEnum()) { - recorder.registerEnum(clazz); - if (isLocalToModule) { - decls.add(extractValueEnum(classInfo, clazz, enumBuilder)); - } - } else { - // Type enums - var variants = index.getComputingIndex().getAllKnownImplementors(classInfo.name()); - var variantClasses = new ArrayList>(); - if (variants.isEmpty()) { - throw new RuntimeException("No variants found for enum: " + enumBuilder.getName()); - } - for (var variant : variants) { - var isVariantLocalToModule = !variant.hasDeclaredAnnotation(GENERATED_REF); - 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(); - } 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); - } - if (isVariantLocalToModule) { - 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()); - } - } - if (isLocalToModule) { - decls.add(Decl.newBuilder().setEnum(enumBuilder).build()); - } - recorder.registerEnum(clazz, variantClasses); - } - } + var decls = extractEnumDecls(index, enumAnnotations, recorder, moduleBuilder); for (var decl : decls) { moduleBuilder.addDecls(decl); } @@ -115,9 +62,45 @@ public void accept(ModuleBuilder moduleBuilder) { }); } - private Decl extractValueEnum(ClassInfo classInfo, Class clazz, Enum.Builder enumBuilder) + /** + * 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) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { - // Value enums must have a type + 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)); + } + } else { + var typeEnum = extractTypeEnum(index, moduleBuilder, classInfo, exported); + recorder.registerEnum(clazz, typeEnum.variantClasses); + if (isLocalToModule) { + decls.add(typeEnum.decl); + } + } + } + return decls; + } + + private Decl extractValueEnum(ClassInfo classInfo, Class clazz, boolean exported) + throws NoSuchFieldException, IllegalAccessException { + Enum.Builder enumBuilder = Enum.newBuilder() + .setName(classInfo.simpleName()) + .setExport(exported); + // Value enums must have a type defined by the 'value' field FieldInfo valueField = classInfo.field("value"); if (valueField == null) { throw new RuntimeException("Enum must have a 'value' field: " + classInfo.name()); @@ -154,6 +137,48 @@ private Decl extractValueEnum(ClassInfo classInfo, Class clazz, Enum.Builder return Decl.newBuilder().setEnum(enumBuilder).build(); } + private record TypeEnum(Decl decl, List> variantClasses) { + } + + 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>(); + 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) { if (type.kind() != Type.Kind.PRIMITIVE) { return false; From b937adc5eda80f648351b30e4d7af58ac1a68170 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 13:20:35 +1000 Subject: [PATCH 28/34] comments --- .../xyz/block/ftl/deployment/EnumProcessor.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index 332c83888d..f27897839b 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -19,6 +19,7 @@ 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; @@ -40,6 +41,7 @@ public class EnumProcessor { private static final Logger log = LoggerFactory.getLogger(EnumProcessor.class); + public static final Set INT_TYPES = Set.of(INT, LONG, BYTE, SHORT); @BuildStep @Record(ExecutionTime.RUNTIME_INIT) @@ -95,12 +97,14 @@ private List extractEnumDecls(CombinedIndexBuildItem index, Collection clazz, boolean exported) throws NoSuchFieldException, IllegalAccessException { Enum.Builder enumBuilder = Enum.newBuilder() .setName(classInfo.simpleName()) .setExport(exported); - // Value enums must have a type defined by the 'value' field FieldInfo valueField = classInfo.field("value"); if (valueField == null) { throw new RuntimeException("Enum must have a 'value' field: " + classInfo.name()); @@ -140,6 +144,11 @@ private Decl extractValueEnum(ClassInfo classInfo, Class clazz, boolean expor 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) throws ClassNotFoundException { Enum.Builder enumBuilder = Enum.newBuilder() @@ -180,10 +189,7 @@ private TypeEnum extractTypeEnum(CombinedIndexBuildItem index, ModuleBuilder mod } private boolean isInt(Type type) { - if (type.kind() != Type.Kind.PRIMITIVE) { - return false; - } - return Set.of(INT, LONG, BYTE, SHORT).contains(type.asPrimitiveType().primitive()); + return type.kind() == Type.Kind.PRIMITIVE && INT_TYPES.contains(type.asPrimitiveType().primitive()); } } From dc6b07eeb3b52a8c656074420fba4c3626be5ea6 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 13:29:34 +1000 Subject: [PATCH 29/34] fix errors --- .../src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index ccc4506ea9..a824de2937 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -613,7 +613,7 @@ private boolean updateData(String name, boolean export) { if (decls.containsKey(name)) { var existing = decls.get(name); if (!existing.hasData()) { - duplicateNameValidationError(name, existing.getEnum().getPos()); + return true; } var merged = existing.getData().toBuilder().setExport(export).build(); decls.put(name, Decl.newBuilder().setData(merged).build()); From 16d77c2e77c24c28c5c2ad814b0ecc212981eb74 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 13:29:47 +1000 Subject: [PATCH 30/34] comments --- .../src/main/java/xyz/block/ftl/deployment/EnumProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index f27897839b..82568ae9a6 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -146,8 +146,8 @@ 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
+ * - 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) throws ClassNotFoundException { From 9fd824ffd1057d113580015a596ab103fe9efc50 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 16:26:09 +1000 Subject: [PATCH 31/34] Don't produce decls for type aliases in other modules --- .../block/ftl/deployment/ModuleBuilder.java | 28 +++++++++++++++---- .../ftl/deployment/TypeAliasProcessor.java | 5 ++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index a824de2937..2fb6d9d42d 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -88,6 +88,7 @@ public class ModuleBuilder { private final IndexView index; private final Module.Builder protoModuleBuilder; private final Map decls = new HashMap<>(); + private final Map externalRefs = new HashMap<>(); private final String moduleName; private final Set knownSecrets = new HashSet<>(); private final Set knownConfig = new HashSet<>(); @@ -391,27 +392,32 @@ public Type buildType(org.jboss.jandex.Type type, boolean export, Nullability nu nullability); } + String name = clazz.name().local(); + if (externalRefs.containsKey(name)) { + // Ref is to another module. Don't need a Decl + return Type.newBuilder().setRef(externalRefs.get(name)).build(); + } var ref = Type.newBuilder().setRef( - Ref.newBuilder().setName(clazz.name().local()).setModule(moduleName).build()).build(); + Ref.newBuilder().setName(name).setModule(moduleName).build()).build(); if (info.isEnum() || info.hasAnnotation(ENUM)) { // Set only the name and export here. EnumProcessor will fill in the rest xyz.block.ftl.v1.schema.Enum ennum = xyz.block.ftl.v1.schema.Enum.newBuilder() - .setName(clazz.name().local()) + .setName(name) .setExport(type.hasAnnotation(EXPORT) || export) .build(); addDecls(Decl.newBuilder().setEnum(ennum).build()); return ref; } else { // If this data was processed already, skip early - if (updateData(clazz.name().local(), type.hasAnnotation(EXPORT) || export)) { + if (updateData(name, type.hasAnnotation(EXPORT) || export)) { return ref; } Data.Builder data = Data.newBuilder(); data.setPos(PositionUtils.forClass(clazz.name().toString())); - data.setName(clazz.name().local()); + data.setName(name); data.setExport(type.hasAnnotation(EXPORT) || export); - Optional.ofNullable(comments.get(CommentKey.ofData(clazz.name().local()))) + Optional.ofNullable(comments.get(CommentKey.ofData(name))) .ifPresent(data::addAllComments); buildDataElement(data, clazz.name()); addDecls(Decl.newBuilder().setData(data).build()); @@ -567,6 +573,18 @@ public void registerTypeAlias(String name, org.jboss.jandex.Type finalT, org.jbo .build()); } + /** + * Types from other modules don't need a Decl. We store Ref for it, and prevent a Decl being created next + * time we see this name + */ + public void registerExternalType(String module, String name) { + Ref ref = Ref.newBuilder() + .setModule(module) + .setName(name) + .build(); + externalRefs.put(name, ref); + } + private void addDecl(Decl decl, Position pos, String name) { validateName(pos, name); if (decls.containsKey(name)) { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java index bd2c57f723..7b40b1f4c2 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java @@ -91,6 +91,11 @@ public void processTypeAlias(CombinedIndexBuildItem index, } schemaContributorBuildItemBuildProducer.produce(new SchemaContributorBuildItem(moduleBuilder -> moduleBuilder .registerTypeAlias(name, finalT, finalS, exported, languageMappings))); + } else { + // If the 'module' field of the annotation is non-empty, we have a mapper for a type alias defined in + // another module. Don't need a Decl + schemaContributorBuildItemBuildProducer.produce(new SchemaContributorBuildItem(moduleBuilder -> moduleBuilder + .registerExternalType(module, name))); } } From 959fd2e8d8795dc7831ca7b3fc225e9a2bde49a1 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 21:38:05 +1000 Subject: [PATCH 32/34] fix integration tests --- jvm-runtime/jvm_integration_test.go | 90 +++++++++++++++++------------ 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/jvm-runtime/jvm_integration_test.go b/jvm-runtime/jvm_integration_test.go index 84da6d4e8d..b83025eccf 100644 --- a/jvm-runtime/jvm_integration_test.go +++ b/jvm-runtime/jvm_integration_test.go @@ -77,35 +77,35 @@ func TestJVMCoreFunctionality(t *testing.T) { } tests := []in.SubTest{} - tests = append(tests, PairedTest("emptyVerb", func(module string) in.Action { + tests = append(tests, AllRuntimesTest("emptyVerb", func(module string) in.Action { return in.Call(module, "emptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) { assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response)) }) })...) - tests = append(tests, PairedTest("sinkVerb", func(module string) in.Action { + tests = append(tests, AllRuntimesTest("sinkVerb", func(module string) in.Action { return in.Call(module, "sinkVerb", "ignored", func(t testing.TB, response in.Obj) { assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response)) }) })...) - tests = append(tests, PairedTest("sourceVerb", func(module string) in.Action { + tests = append(tests, AllRuntimesTest("sourceVerb", func(module string) in.Action { return in.Call(module, "sourceVerb", in.Obj{}, func(t testing.TB, response string) { assert.Equal(t, "Source Verb", response, "expecting empty response, got %s", response) }) })...) - tests = append(tests, PairedTest("errorEmptyVerb", func(module string) in.Action { + tests = append(tests, AllRuntimesTest("errorEmptyVerb", func(module string) in.Action { return in.Fail( in.Call(module, "errorEmptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) { assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response)) }), "verb failed") })...) - tests = append(tests, PairedVerbTest("intVerb", 124)...) - tests = append(tests, PairedVerbTest("floatVerb", 0.123)...) - tests = append(tests, PairedVerbTest("stringVerb", "Hello World")...) - tests = append(tests, PairedVerbTest("bytesVerb", []byte{1, 2, 3, 0, 1})...) - tests = append(tests, PairedVerbTest("boolVerb", true)...) - tests = append(tests, PairedVerbTest("stringArrayVerb", []string{"Hello World"})...) - tests = append(tests, PairedVerbTest("stringMapVerb", map[string]string{"Hello": "World"})...) - tests = append(tests, PairedTest("timeVerb", func(module string) in.Action { + tests = append(tests, AllRuntimesVerbTest("intVerb", 124)...) + tests = append(tests, AllRuntimesVerbTest("floatVerb", 0.123)...) + tests = append(tests, AllRuntimesVerbTest("stringVerb", "Hello World")...) + tests = append(tests, AllRuntimesVerbTest("bytesVerb", []byte{1, 2, 3, 0, 1})...) + tests = append(tests, AllRuntimesVerbTest("boolVerb", true)...) + tests = append(tests, AllRuntimesVerbTest("stringArrayVerb", []string{"Hello World"})...) + tests = append(tests, AllRuntimesVerbTest("stringMapVerb", map[string]string{"Hello": "World"})...) + tests = append(tests, AllRuntimesTest("timeVerb", func(module string) in.Action { now := time.Now().UTC() return in.Call(module, "timeVerb", now.Format(time.RFC3339Nano), func(t testing.TB, response string) { result, err := time.Parse(time.RFC3339Nano, response) @@ -113,19 +113,19 @@ func TestJVMCoreFunctionality(t *testing.T) { assert.Equal(t, now, result, "times not equal %s %s", now, result) }) })...) - tests = append(tests, PairedVerbTest("testObjectVerb", exampleObject)...) - tests = append(tests, PairedVerbTest("testObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...) - tests = append(tests, PairedVerbTest("objectMapVerb", map[string]TestObject{"hello": exampleObject})...) - tests = append(tests, PairedVerbTest("objectArrayVerb", []TestObject{exampleObject})...) - tests = append(tests, PairedVerbTest("parameterizedObjectVerb", parameterizedObject)...) - tests = append(tests, PairedVerbTest("optionalIntVerb", -3)...) - tests = append(tests, PairedVerbTest("optionalFloatVerb", -7.6)...) - tests = append(tests, PairedVerbTest("optionalStringVerb", "foo")...) - tests = append(tests, PairedVerbTest("optionalBytesVerb", []byte{134, 255, 0})...) - tests = append(tests, PairedVerbTest("optionalBoolVerb", false)...) - tests = append(tests, PairedVerbTest("optionalStringArrayVerb", []string{"foo"})...) - tests = append(tests, PairedVerbTest("optionalStringMapVerb", map[string]string{"Hello": "World"})...) - tests = append(tests, PairedTest("optionalTimeVerb", func(module string) in.Action { + tests = append(tests, AllRuntimesVerbTest("testObjectVerb", exampleObject)...) + tests = append(tests, AllRuntimesVerbTest("testObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...) + tests = append(tests, AllRuntimesVerbTest("objectMapVerb", map[string]TestObject{"hello": exampleObject})...) + tests = append(tests, AllRuntimesVerbTest("objectArrayVerb", []TestObject{exampleObject})...) + tests = append(tests, AllRuntimesVerbTest("parameterizedObjectVerb", parameterizedObject)...) + tests = append(tests, AllRuntimesVerbTest("optionalIntVerb", -3)...) + tests = append(tests, AllRuntimesVerbTest("optionalFloatVerb", -7.6)...) + tests = append(tests, AllRuntimesVerbTest("optionalStringVerb", "foo")...) + tests = append(tests, AllRuntimesVerbTest("optionalBytesVerb", []byte{134, 255, 0})...) + tests = append(tests, AllRuntimesVerbTest("optionalBoolVerb", false)...) + tests = append(tests, AllRuntimesVerbTest("optionalStringArrayVerb", []string{"foo"})...) + tests = append(tests, AllRuntimesVerbTest("optionalStringMapVerb", map[string]string{"Hello": "World"})...) + tests = append(tests, AllRuntimesTest("optionalTimeVerb", func(module string) in.Action { now := time.Now().UTC() return in.Call(module, "optionalTimeVerb", now.Format(time.RFC3339Nano), func(t testing.TB, response string) { result, err := time.Parse(time.RFC3339Nano, response) @@ -134,10 +134,10 @@ func TestJVMCoreFunctionality(t *testing.T) { }) })...) - tests = append(tests, PairedVerbTest("optionalTestObjectVerb", exampleObject)...) - tests = append(tests, PairedVerbTest("optionalTestObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...) - tests = append(tests, PairedVerbTest("externalTypeVerb", "did:web:abc123")...) - tests = append(tests, PairedVerbTest("typeEnumVerb", AnimalWrapper{Animal: Animal{ + tests = append(tests, AllRuntimesVerbTest("optionalTestObjectVerb", exampleObject)...) + tests = append(tests, AllRuntimesVerbTest("optionalTestObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...) + tests = append(tests, AllRuntimesVerbTest("externalTypeVerb", "did:web:abc123")...) + tests = append(tests, JavaAndGoVerbTest("typeEnumVerb", AnimalWrapper{Animal: Animal{ Name: "Cat", Value: Cat{ Name: "Fluffy", @@ -145,9 +145,9 @@ func TestJVMCoreFunctionality(t *testing.T) { Breed: "Siamese", }, }})...) - tests = append(tests, PairedVerbTest("valueEnumVerb", ColorWrapper{Color: Red})...) - //tests = append(tests, PairedVerbTest("typeWrapperEnumVerb", "hello")...) - //tests = append(tests, PairedVerbTest("mixedEnumVerb", Thing{})...) + tests = append(tests, JavaAndGoVerbTest("valueEnumVerb", ColorWrapper{Color: Red})...) + //tests = append(tests, AllRuntimesVerbTest("typeWrapperEnumVerb", "hello")...) + //tests = append(tests, AllRuntimesVerbTest("mixedEnumVerb", Thing{})...) // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalIntVerb", ftl.None[int]())...) // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalFloatVerb", ftl.None[float64]())...) // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalStringVerb", ftl.None[string]())...) @@ -281,7 +281,20 @@ func TestGradle(t *testing.T) { ) } -func PairedTest(name string, testFunc func(module string) in.Action) []in.SubTest { +func JavaAndGoTest(name string, testFunc func(module string) in.Action) []in.SubTest { + return []in.SubTest{ + { + Name: name + "-go", + Action: testFunc("gomodule"), + }, + { + Name: name + "-java", + Action: testFunc("javaclient"), + }, + } +} + +func AllRuntimesTest(name string, testFunc func(module string) in.Action) []in.SubTest { return []in.SubTest{ { Name: name + "-go", @@ -302,7 +315,7 @@ func JVMTest(name string, testFunc func(name string, module string) in.Action) [ return []in.SubTest{ { Name: name + "-java", - Action: testFunc(name, "javamodule"), + Action: testFunc(name, "javaclient"), }, { Name: name + "-kotlin", @@ -319,12 +332,15 @@ func VerbTest[T any](verb string, value T) func(module string) in.Action { } } -func PairedVerbTest[T any](verb string, value T) []in.SubTest { - return PairedTest(verb, VerbTest[T](verb, value)) +func AllRuntimesVerbTest[T any](verb string, value T) []in.SubTest { + return AllRuntimesTest(verb, VerbTest[T](verb, value)) +} +func JavaAndGoVerbTest[T any](verb string, value T) []in.SubTest { + return JavaAndGoTest(verb, VerbTest[T](verb, value)) } func PairedPrefixVerbTest[T any](prefex string, verb string, value T) []in.SubTest { - return PairedTest(prefex+"-"+verb, VerbTest[T](verb, value)) + return AllRuntimesTest(prefex+"-"+verb, VerbTest[T](verb, value)) } type TestObject struct { From b8b1b503f1f0981ff116958f459ee247f71cd39a Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Wed, 9 Oct 2024 21:46:09 +1000 Subject: [PATCH 33/34] cleanup --- .../block/ftl/deployment/EnumProcessor.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index 82568ae9a6..1a140ffda7 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -48,18 +48,14 @@ public class EnumProcessor { 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() { - @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); + 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); } }); } From 5f0f0a3252548c92cb089144b28bd9db921118e5 Mon Sep 17 00:00:00 2001 From: Tom Daffurn Date: Thu, 10 Oct 2024 09:01:31 +1000 Subject: [PATCH 34/34] change to jboss logger --- .../xyz/block/ftl/deployment/DatasourceProcessor.java | 7 +++---- .../java/xyz/block/ftl/deployment/EnumProcessor.java | 8 +++----- .../xyz/block/ftl/deployment/JVMCodeGenerator.java | 5 ++--- .../xyz/block/ftl/deployment/ModuleProcessor.java | 9 ++++----- .../block/ftl/deployment/SubscriptionProcessor.java | 9 ++++----- .../xyz/block/ftl/deployment/TopicsProcessor.java | 7 +++---- .../xyz/block/ftl/deployment/TypeAliasProcessor.java | 7 +++---- .../java/xyz/block/ftl/deployment/VerbProcessor.java | 11 +++++------ 8 files changed, 27 insertions(+), 36 deletions(-) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java index c2dcd99ffe..b0f72df4b6 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java @@ -4,8 +4,7 @@ import java.util.ArrayList; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -19,14 +18,14 @@ public class DatasourceProcessor { - private static final Logger log = LoggerFactory.getLogger(DatasourceProcessor.class); + private static final Logger log = Logger.getLogger(DatasourceProcessor.class); @BuildStep public SchemaContributorBuildItem registerDatasources( List datasources, BuildProducer systemPropProducer, BuildProducer generatedResourceBuildItemBuildProducer) { - log.info("Processing {} datasource annotations into decls", datasources.size()); + log.infof("Processing %d datasource annotations into decls", datasources.size()); List decls = new ArrayList<>(); List namedDatasources = new ArrayList<>(); for (var ds : datasources) { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java index 1a140ffda7..04a1db79a6 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/EnumProcessor.java @@ -12,7 +12,6 @@ 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; @@ -21,8 +20,7 @@ import org.jboss.jandex.FieldInfo; import org.jboss.jandex.PrimitiveType; import org.jboss.jandex.Type; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -40,14 +38,14 @@ public class EnumProcessor { - private static final Logger log = LoggerFactory.getLogger(EnumProcessor.class); + 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) { var enumAnnotations = index.getIndex().getAnnotations(FTLDotNames.ENUM); - log.info("Processing {} enum annotations into decls", enumAnnotations.size()); + log.infof("Processing %d enum annotations into decls", enumAnnotations.size()); return new SchemaContributorBuildItem(moduleBuilder -> { try { var decls = extractEnumDecls(index, enumAnnotations, recorder, moduleBuilder); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java index 10268429ab..47a4f1ef44 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/JVMCodeGenerator.java @@ -11,8 +11,7 @@ import java.util.stream.Stream; import org.eclipse.microprofile.config.Config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import io.quarkus.bootstrap.prebuild.CodeGenException; import io.quarkus.deployment.CodeGenContext; @@ -29,7 +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 = LoggerFactory.getLogger(JVMCodeGenerator.class); + private static final Logger log = Logger.getLogger(JVMCodeGenerator.class); @Override public String providerId() { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java index 3240faaf44..41f230d260 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java @@ -15,8 +15,7 @@ import java.util.stream.Collectors; import org.jboss.jandex.DotName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import org.tomlj.Toml; import org.tomlj.TomlParseResult; @@ -47,7 +46,7 @@ public class ModuleProcessor { - private static final Logger log = LoggerFactory.getLogger(ModuleProcessor.class); + private static final Logger log = Logger.getLogger(ModuleProcessor.class); private static final String FEATURE = "ftl-java-runtime"; @@ -88,7 +87,7 @@ ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem if (value != null) { return new ModuleNameBuildItem(value); } else { - log.error("module name not found in {}", toml); + log.errorf("module name not found in %s", toml); } } if (source.getParent() == null) { @@ -123,7 +122,7 @@ public void generateSchema(CombinedIndexBuildItem index, i.getSchemaContributor().accept(moduleBuilder); } - log.info("Generating module '{}' schema from {} decls", moduleName, moduleBuilder.getDeclsCount()); + log.infof("Generating module '%s' schema from %d decls", moduleName, moduleBuilder.getDeclsCount()); Path output = outputTargetBuildItem.getOutputDirectory().resolve(SCHEMA_OUT); try (var out = Files.newOutputStream(output)) { moduleBuilder.writeTo(out); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java index 247fb594bc..bfd17e2121 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java @@ -8,8 +8,7 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -25,14 +24,14 @@ public class SubscriptionProcessor { - private static final Logger log = LoggerFactory.getLogger(SubscriptionProcessor.class); + private static final Logger log = Logger.getLogger(SubscriptionProcessor.class); @BuildStep SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildItem combinedIndexBuildItem, ModuleNameBuildItem moduleNameBuildItem) { Collection subscriptionAnnotations = combinedIndexBuildItem.getComputingIndex() .getAnnotations(Subscription.class); - log.info("Processing {} subscription annotations into decls", subscriptionAnnotations.size()); + log.infof("Processing %s subscription annotations into decls", subscriptionAnnotations.size()); Map annotations = new HashMap<>(); for (var subscriptions : subscriptionAnnotations) { if (subscriptions.target().kind() != AnnotationTarget.Kind.CLASS) { @@ -65,7 +64,7 @@ public void registerSubscriptions(CombinedIndexBuildItem index, for (var metaSub : subscriptionMetaAnnotationsBuildItem.getAnnotations().entrySet()) { for (var subscription : index.getIndex().getAnnotations(metaSub.getKey())) { if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { - log.warn("Subscription annotation on non-method target: {}", subscription.target()); + log.warnf("Subscription annotation on non-method target: %s", subscription.target()); continue; } var method = subscription.target().asMethod(); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java index 91efb23bc2..dda47644dd 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java @@ -8,8 +8,7 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.Type; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; @@ -27,12 +26,12 @@ public class TopicsProcessor { public static final DotName TOPIC = DotName.createSimple(Topic.class); - private static final Logger log = LoggerFactory.getLogger(TopicsProcessor.class); + private static final Logger log = Logger.getLogger(TopicsProcessor.class); @BuildStep TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer generatedTopicProducer) { var topicDefinitions = index.getComputingIndex().getAnnotations(TopicDefinition.class); - log.info("Processing {} topic definition annotations into decls", topicDefinitions.size()); + log.infof("Processing %d topic definition annotations into decls", topicDefinitions.size()); Map topics = new HashMap<>(); Set names = new HashSet<>(); for (var topicDefinition : topicDefinitions) { diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java index 7b40b1f4c2..8016651076 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java @@ -8,8 +8,7 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.Type; import org.jboss.jandex.TypeVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -18,7 +17,7 @@ public class TypeAliasProcessor { - private static final Logger log = LoggerFactory.getLogger(TypeAliasProcessor.class); + private static final Logger log = Logger.getLogger(TypeAliasProcessor.class); @BuildStep public void processTypeAlias(CombinedIndexBuildItem index, @@ -26,7 +25,7 @@ public void processTypeAlias(CombinedIndexBuildItem index, BuildProducer additionalBeanBuildItem, BuildProducer typeAliasBuildItemBuildProducer) { Collection typeAliasAnnotations = index.getIndex().getAnnotations(FTLDotNames.TYPE_ALIAS); - log.info("Processing {} type alias annotations into decls", typeAliasAnnotations.size()); + log.infof("Processing %d type alias annotations into decls", typeAliasAnnotations.size()); var beans = new AdditionalBeanBuildItem.Builder().setUnremovable(); for (var mapper : typeAliasAnnotations) { boolean exported = mapper.target().hasAnnotation(FTLDotNames.EXPORT); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java index 9b3480a0c5..0fc435ecdc 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java @@ -12,8 +12,7 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; import org.jboss.jandex.Type; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; @@ -43,7 +42,7 @@ public class VerbProcessor { public static final DotName VERB_CLIENT_SOURCE = DotName.createSimple(VerbClientSource.class); public static final DotName VERB_CLIENT_EMPTY = DotName.createSimple(VerbClientEmpty.class); public static final String TEST_ANNOTATION = "xyz.block.ftl.java.test.FTLManaged"; - private static final Logger log = LoggerFactory.getLogger(VerbProcessor.class); + private static final Logger log = Logger.getLogger(VerbProcessor.class); @BuildStep VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProducer generatedClients, @@ -51,7 +50,7 @@ VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProduce ModuleNameBuildItem moduleNameBuildItem, LaunchModeBuildItem launchModeBuildItem) { var clientDefinitions = index.getComputingIndex().getAnnotations(VerbClientDefinition.class); - log.info("Processing {} verb clients", clientDefinitions.size()); + log.infof("Processing %d verb clients", clientDefinitions.size()); Map clients = new HashMap<>(); for (var clientDefinition : clientDefinitions) { var iface = clientDefinition.target().asClass(); @@ -236,7 +235,7 @@ public void verbsAndCron(CombinedIndexBuildItem index, List typeAliasBuildItems // included to force typealias processing before this ) { Collection verbAnnotations = index.getIndex().getAnnotations(FTLDotNames.VERB); - log.info("Processing {} verb annotations into decls", verbAnnotations.size()); + log.infof("Processing %d verb annotations into decls", verbAnnotations.size()); var beans = AdditionalBeanBuildItem.builder().setUnremovable(); for (var verb : verbAnnotations) { boolean exported = verb.target().hasAnnotation(FTLDotNames.EXPORT); @@ -248,7 +247,7 @@ public void verbsAndCron(CombinedIndexBuildItem index, } Collection cronAnnotations = index.getIndex().getAnnotations(FTLDotNames.CRON); - log.info("Processing {} cron job annotations into decls", cronAnnotations.size()); + log.infof("Processing %d cron job annotations into decls", cronAnnotations.size()); for (var cron : cronAnnotations) { var method = cron.target().asMethod(); String className = method.declaringClass().name().toString();