From b62128f305dcb83d831252063ed51dad9c470788 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Wed, 28 Aug 2024 10:33:23 -0400 Subject: [PATCH] fix: use typemap in JVM fixes: #2513 fixes: #2510 --- go.mod | 2 + go.sum | 8 ++ .../xyz/block/ftl/deployment/FTLDotNames.java | 4 + .../ftl/deployment/JVMCodeGenerator.java | 46 ++++++++-- .../block/ftl/deployment/ModuleBuilder.java | 18 ++-- .../block/ftl/deployment/ModuleProcessor.java | 21 ++++- .../ftl/deployment/TypeAliasBuildItem.java | 42 +++++++++ .../ftl/deployment/TypeAliasProcessor.java | 81 +++++++++++++++++ .../xyz/block/ftl/deployment/TypeKey.java | 7 ++ .../ftl-runtime/common/runtime/pom.xml | 5 ++ .../main/java/xyz/block/ftl/TypeAlias.java | 15 ++++ .../java/xyz/block/ftl/TypeAliasMapper.java | 9 ++ .../ftl/runtime/JsonSerializationConfig.java | 89 +++++++++++++++++++ .../xyz/block/ftl/runtime/VerbHandler.java | 5 ++ .../runtime/JsonSerializationConfigTest.java | 72 +++++++++++++++ .../deployment/JavaCodeGenerator.java | 82 ++++++++++++----- .../deployment/KotlinCodeGenerator.java | 79 +++++++++++----- jvm-runtime/jvm_integration_test.go | 8 ++ jvm-runtime/testdata/go/gomodule/go.mod | 6 +- jvm-runtime/testdata/go/gomodule/go.sum | 9 ++ jvm-runtime/testdata/go/gomodule/server.go | 16 ++++ jvm-runtime/testdata/java/passthrough/pom.xml | 9 +- .../java/xyz/block/ftl/test/DidMapper.java | 19 ++++ .../block/ftl/test/TestInvokeGoFromJava.java | 8 ++ .../testdata/kotlin/passthrough/pom.xml | 8 ++ .../kotlin/xyz/block/ftl/test/DidMapper.kt | 18 ++++ .../block/ftl/test/TestInvokeGoFromKotlin.kt | 7 ++ 27 files changed, 629 insertions(+), 64 deletions(-) create mode 100644 jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasBuildItem.java create mode 100644 jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java create mode 100644 jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeKey.java create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAlias.java create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAliasMapper.java create mode 100644 jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/JsonSerializationConfigTest.java create mode 100644 jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/DidMapper.java create mode 100644 jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/DidMapper.kt diff --git a/go.mod b/go.mod index 1b1f60d3dc..c4e88f77d6 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/sqlc-dev/pqtype v0.3.0 github.com/swaggest/jsonschema-go v0.3.72 + github.com/tbd54566975/web5-go v0.24.0 github.com/tink-crypto/tink-go-awskms v0.0.0-20230616072154-ba4f9f22c3e9 github.com/tink-crypto/tink-go/v2 v2.2.0 github.com/titanous/json5 v1.0.0 @@ -85,6 +86,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect diff --git a/go.sum b/go.sum index af02f107a7..de0a5403e4 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= @@ -253,6 +257,8 @@ github.com/swaggest/jsonschema-go v0.3.72 h1:IHaGlR1bdBUBPfhe4tfacN2TGAPKENEGiNy github.com/swaggest/jsonschema-go v0.3.72/go.mod h1:OrGyEoVqpfSFJ4Am4V/FQcQ3mlEC1vVeleA+5ggbVW4= github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/tbd54566975/web5-go v0.24.0 h1:21pLJCeMRt1hEdedzu3FpSJLsnk+MZ2SOxKYQxj3gC8= +github.com/tbd54566975/web5-go v0.24.0/go.mod h1:7M5TGxGZY4g/eh0aoSD1h6AGr5yJXAc706wSquc8QsE= github.com/tink-crypto/tink-go-awskms v0.0.0-20230616072154-ba4f9f22c3e9 h1:MoIsYvBNJd8vkKZjLYloE3OK8bfcO10cMPw/EtydMBs= github.com/tink-crypto/tink-go-awskms v0.0.0-20230616072154-ba4f9f22c3e9/go.mod h1:TTE4PoQLsYB5jQ1kK2g7WU4wzHg0Arn1CEozIUXiGSY= github.com/tink-crypto/tink-go/v2 v2.2.0 h1:L2Da0F2Udh2agtKztdr69mV/KpnY3/lGTkMgLTVIXlA= @@ -267,6 +273,8 @@ github.com/tliron/kutil v0.3.26 h1:G+dicQLvzm3zdOMrrQFLBfHJXtk57fEu2kf1IFNyJxw= github.com/tliron/kutil v0.3.26/go.mod h1:1/HRVAb+fnRIRnzmhu0FPP+ZJKobrpwHStDVMuaXDzY= github.com/tmc/langchaingo v0.1.12 h1:yXwSu54f3b1IKw0jJ5/DWu+qFVH1NBblwC0xddBzGJE= github.com/tmc/langchaingo v0.1.12/go.mod h1:cd62xD6h+ouk8k/QQFhOsjRYBSA1JJ5UVKXSIgm7Ni4= +github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa h1:2EwhXkNkeMjX9iFYGWLPQLPhw9O58BhnYgtYKeqybcY= +github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa/go.mod h1:is48sjgBanWcA5CQrPBu9Y5yABY/T2awj/zI65bq704= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= 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 d469abb599..42de5a1d6e 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 @@ -8,6 +8,8 @@ import xyz.block.ftl.LeaseClient; import xyz.block.ftl.Secret; import xyz.block.ftl.Subscription; +import xyz.block.ftl.TypeAlias; +import xyz.block.ftl.TypeAliasMapper; import xyz.block.ftl.Verb; public class FTLDotNames { @@ -21,6 +23,8 @@ private FTLDotNames() { public static final DotName EXPORT = DotName.createSimple(Export.class); public static final DotName VERB = DotName.createSimple(Verb.class); public static final DotName CRON = DotName.createSimple(Cron.class); + public static final DotName TYPE_ALIAS_MAPPER = DotName.createSimple(TypeAliasMapper.class); + public static final DotName TYPE_ALIAS = DotName.createSimple(TypeAlias.class); public static final DotName SUBSCRIPTION = DotName.createSimple(Subscription.class); public static final DotName LEASE_CLIENT = DotName.createSimple(LeaseClient.class); } 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 5abd1a156e..60347f4cae 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 @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import org.eclipse.microprofile.config.Config; @@ -24,6 +25,7 @@ public abstract class JVMCodeGenerator implements CodeGenProvider { public static final String PACKAGE_PREFIX = "ftl."; + public static final String TYPE_MAPPER = "TypeAliasMapper"; @Override public String providerId() { @@ -42,6 +44,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { } List modules = new ArrayList<>(); Map typeAliasMap = new HashMap<>(); + Map nativeTypeAliasMap = new HashMap<>(); try (Stream pathStream = Files.list(context.inputDir())) { for (var file : pathStream.toList()) { String fileName = file.getFileName().toString(); @@ -50,9 +53,30 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { } var module = Module.parseFrom(Files.readAllBytes(file)); for (var decl : module.getDeclsList()) { + String packageName = PACKAGE_PREFIX + module.getName(); if (decl.hasTypeAlias()) { var data = decl.getTypeAlias(); - typeAliasMap.put(new DeclRef(module.getName(), data.getName()), data.getType()); + boolean handled = false; + for (var md : data.getMetadataList()) { + if (md.hasTypeMap()) { + String runtime = md.getTypeMap().getRuntime(); + if (runtime.equals("kotlin") || runtime.equals("java")) { + nativeTypeAliasMap.put(new DeclRef(module.getName(), data.getName()), + md.getTypeMap().getNativeName()); + generateTypeAliasMapper(module.getName(), data.getName(), packageName, + Optional.of(md.getTypeMap().getNativeName()), + context.outDir()); + handled = true; + break; + } + } + } + if (!handled) { + generateTypeAliasMapper(module.getName(), data.getName(), packageName, Optional.empty(), + context.outDir()); + typeAliasMap.put(new DeclRef(module.getName(), data.getName()), data.getType()); + } + } } modules.add(module); @@ -69,26 +93,27 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { if (!verb.getExport()) { continue; } - generateVerb(module, verb, packageName, typeAliasMap, context.outDir()); + generateVerb(module, verb, packageName, typeAliasMap, nativeTypeAliasMap, context.outDir()); } else if (decl.hasData()) { var data = decl.getData(); if (!data.getExport()) { continue; } - generateDataObject(module, data, packageName, typeAliasMap, context.outDir()); + generateDataObject(module, data, packageName, typeAliasMap, nativeTypeAliasMap, context.outDir()); } else if (decl.hasEnum()) { var data = decl.getEnum(); if (!data.getExport()) { continue; } - generateEnum(module, data, packageName, typeAliasMap, context.outDir()); + generateEnum(module, data, packageName, typeAliasMap, nativeTypeAliasMap, context.outDir()); } else if (decl.hasTopic()) { var data = decl.getTopic(); if (!data.getExport()) { continue; } - generateTopicSubscription(module, data, packageName, typeAliasMap, context.outDir()); + generateTopicSubscription(module, data, packageName, typeAliasMap, nativeTypeAliasMap, + context.outDir()); } } } @@ -99,17 +124,20 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { return true; } + protected abstract void generateTypeAliasMapper(String module, String name, String packageName, + Optional nativeTypeAlias, Path outputDir) throws IOException; + protected abstract void generateTopicSubscription(Module module, Topic data, String packageName, - Map typeAliasMap, Path outputDir) throws IOException; + Map typeAliasMap, Map nativeTypeAliasMap, Path outputDir) throws IOException; protected abstract void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Path outputDir) throws IOException; + Map nativeTypeAliasMap, Path outputDir) throws IOException; protected abstract void generateDataObject(Module module, Data data, String packageName, Map typeAliasMap, - Path outputDir) throws IOException; + Map nativeTypeAliasMap, Path outputDir) throws IOException; protected abstract void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, - Path outputDir) throws IOException; + Map nativeTypeAliasMap, Path outputDir) throws IOException; @Override public boolean shouldRun(Path sourceDir, Config config) { 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 cae1ba5f5c..dba6f6acd4 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 @@ -53,11 +53,13 @@ import xyz.block.ftl.v1.schema.Metadata; import xyz.block.ftl.v1.schema.MetadataAlias; import xyz.block.ftl.v1.schema.MetadataCalls; +import xyz.block.ftl.v1.schema.MetadataTypeMap; import xyz.block.ftl.v1.schema.Module; import xyz.block.ftl.v1.schema.Optional; import xyz.block.ftl.v1.schema.Ref; import xyz.block.ftl.v1.schema.Time; import xyz.block.ftl.v1.schema.Type; +import xyz.block.ftl.v1.schema.TypeAlias; import xyz.block.ftl.v1.schema.Unit; import xyz.block.ftl.v1.schema.Verb; @@ -75,7 +77,7 @@ public class ModuleBuilder { private final IndexView index; private final Module.Builder moduleBuilder; - private final Map dataElements = new HashMap<>(); + private final Map dataElements; private final String moduleName; private final Set knownSecrets = new HashSet<>(); private final Set knownConfig = new HashSet<>(); @@ -86,7 +88,7 @@ public class ModuleBuilder { public ModuleBuilder(IndexView index, String moduleName, Map knownTopics, Map verbClients, FTLRecorder recorder, - Map verbDocs) { + Map verbDocs, Map typeAliases) { this.index = index; this.moduleName = moduleName; this.moduleBuilder = Module.newBuilder() @@ -96,6 +98,7 @@ public ModuleBuilder(IndexView index, String moduleName, Map(typeAliases); } public static @NotNull String methodToName(MethodInfo method) { @@ -435,11 +438,16 @@ public void writeTo(OutputStream out) throws IOException { moduleBuilder.build().writeTo(out); } - record ExistingRef(Ref ref, boolean exported) { - + public void registerTypeAlias(String name, org.jboss.jandex.Type finalT, org.jboss.jandex.Type finalS, boolean exported) { + moduleBuilder.addDecls(Decl.newBuilder() + .setTypeAlias(TypeAlias.newBuilder().setType(buildType(finalS, exported)).setName(name).addMetadata(Metadata + .newBuilder() + .setTypeMap(MetadataTypeMap.newBuilder().setRuntime("java").setNativeName(finalT.toString()).build()) + .build())) + .build()); } - private record TypeKey(String name, List typeParams) { + 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 88e7a979a5..339c018e61 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 @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; import org.jboss.logging.Logger; import org.tomlj.Toml; import org.tomlj.TomlParseResult; @@ -42,6 +43,7 @@ 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 { @@ -108,6 +110,7 @@ public void generateSchema(CombinedIndexBuildItem index, ModuleNameBuildItem moduleNameBuildItem, TopicsBuildItem topicsBuildItem, VerbClientBuildItem verbClientBuildItem, + List typeAliasBuildItems, List schemaContributorBuildItems) throws Exception { String moduleName = moduleNameBuildItem.getModuleName(); Map verbDocs = new HashMap<>(); @@ -125,9 +128,25 @@ public void generateSchema(CombinedIndexBuildItem index, } } } + 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, verbDocs); + verbClientBuildItem.getVerbClients(), recorder, verbDocs, existingRefs); for (var i : schemaContributorBuildItems) { i.getSchemaContributor().accept(moduleBuilder); diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasBuildItem.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasBuildItem.java new file mode 100644 index 0000000000..e27babcdbe --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasBuildItem.java @@ -0,0 +1,42 @@ +package xyz.block.ftl.deployment; + +import org.jboss.jandex.Type; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class TypeAliasBuildItem extends MultiBuildItem { + + final String name; + final String module; + final Type localType; + final Type serializedType; + final boolean exported; + + public TypeAliasBuildItem(String name, String module, Type localType, Type serializedType, boolean exported) { + this.name = name; + this.module = module; + this.localType = localType; + this.serializedType = serializedType; + this.exported = exported; + } + + public String getName() { + return name; + } + + public String getModule() { + return module; + } + + public Type getLocalType() { + return localType; + } + + public Type getSerializedType() { + return serializedType; + } + + public boolean isExported() { + return exported; + } +} 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 new file mode 100644 index 0000000000..299edb5b56 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeAliasProcessor.java @@ -0,0 +1,81 @@ +package xyz.block.ftl.deployment; + +import org.jboss.jandex.Type; +import org.jboss.jandex.TypeVariable; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; + +public class TypeAliasProcessor { + + @BuildStep + public void processTypeAlias(CombinedIndexBuildItem index, + BuildProducer schemaContributorBuildItemBuildProducer, + BuildProducer additionalBeanBuildItem, + BuildProducer typeAliasBuildItemBuildProducer) { + var beans = new AdditionalBeanBuildItem.Builder().setUnremovable(); + for (var mapper : index.getIndex().getAnnotations(FTLDotNames.TYPE_ALIAS)) { + boolean exported = mapper.target().hasAnnotation(FTLDotNames.EXPORT); + // This may or may not be the actual mapper, it may be a subclass + + var mapperClass = mapper.target().asClass(); + var actualMapper = mapperClass; + + Type t = null; + Type s = null; + if (mapperClass.isInterface()) { + for (var i : mapperClass.interfaceTypes()) { + if (i.name().equals(FTLDotNames.TYPE_ALIAS_MAPPER)) { + t = i.asParameterizedType().arguments().get(0); + s = i.asParameterizedType().arguments().get(1); + break; + } + } + var implementations = index.getComputingIndex().getAllKnownImplementors(mapperClass.name()); + if (implementations.isEmpty()) { + continue; + } + if (implementations.size() > 1) { + throw new RuntimeException( + "Multiple implementations of " + mapperClass.name() + " found: " + implementations); + } + actualMapper = implementations.iterator().next(); + } + + //TODO: this is a bit hacky and won't work for complex heirachies + // it is enough to get us going through + for (var i : actualMapper.interfaceTypes()) { + if (i.name().equals(FTLDotNames.TYPE_ALIAS_MAPPER)) { + t = i.asParameterizedType().arguments().get(0); + s = i.asParameterizedType().arguments().get(1); + break; + } else if (i.name().equals(mapperClass.name())) { + if (t instanceof TypeVariable) { + t = i.asParameterizedType().arguments().get(0); + } + if (s instanceof TypeVariable) { + s = i.asParameterizedType().arguments().get(1); + } + break; + } + } + + beans.addBeanClass(actualMapper.name().toString()); + var finalT = t; + var finalS = s; + String module = mapper.value("module") == null ? "" : mapper.value("module").asString(); + String name = mapper.value("name").asString(); + typeAliasBuildItemBuildProducer.produce(new TypeAliasBuildItem(name, module, t, s, exported)); + if (module.isEmpty()) { + schemaContributorBuildItemBuildProducer.produce(new SchemaContributorBuildItem(moduleBuilder -> moduleBuilder + .registerTypeAlias(name, finalT, finalS, exported))); + } + + } + additionalBeanBuildItem.produce(beans.build()); + + } + +} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeKey.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeKey.java new file mode 100644 index 0000000000..311928a12e --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TypeKey.java @@ -0,0 +1,7 @@ +package xyz.block.ftl.deployment; + +import java.util.List; + +record TypeKey(String name, List typeParams) { + +} diff --git a/jvm-runtime/ftl-runtime/common/runtime/pom.xml b/jvm-runtime/ftl-runtime/common/runtime/pom.xml index 1d0d37110b..d8b58c7940 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/pom.xml +++ b/jvm-runtime/ftl-runtime/common/runtime/pom.xml @@ -57,6 +57,11 @@ org.jetbrains annotations + + org.junit.jupiter + junit-jupiter + test + diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAlias.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAlias.java new file mode 100644 index 0000000000..1eac64675f --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAlias.java @@ -0,0 +1,15 @@ +package xyz.block.ftl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface TypeAlias { + + String name(); + + String module() default ""; +} diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAliasMapper.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAliasMapper.java new file mode 100644 index 0000000000..a468b7e17a --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/TypeAliasMapper.java @@ -0,0 +1,9 @@ +package xyz.block.ftl; + +public interface TypeAliasMapper { + + S encode(T object); + + T decode(S serialized); + +} 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 a4343b4413..ff8e683671 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,11 +1,17 @@ package xyz.block.ftl.runtime; import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.Base64; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationContext; @@ -20,6 +26,7 @@ import io.quarkus.arc.Unremovable; import io.quarkus.jackson.ObjectMapperCustomizer; +import xyz.block.ftl.TypeAliasMapper; /** * This class configures the FTL serialization @@ -28,6 +35,13 @@ @Unremovable public class JsonSerializationConfig implements ObjectMapperCustomizer { + final Instance> instances; + + @Inject + public JsonSerializationConfig(Instance> instances) { + this.instances = instances; + } + @Override public void customize(ObjectMapper mapper) { mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); @@ -35,9 +49,47 @@ public void customize(ObjectMapper mapper) { module.addSerializer(byte[].class, new ByteArraySerializer()); module.addDeserializer(byte[].class, new ByteArrayDeserializer()); mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + for (var i : instances) { + var object = extractTypeAliasParam(i.getClass(), 0); + var serialized = extractTypeAliasParam(i.getClass(), 1); + module.addSerializer(object, new TypeAliasSerializer(object, serialized, i)); + module.addDeserializer(object, new TypeAliasDeSerializer(object, serialized, i)); + } mapper.registerModule(module); } + static Class extractTypeAliasParam(Class target, int no) { + return (Class) extractTypeAliasParamImpl(target, no); + } + + static Type extractTypeAliasParamImpl(Class target, int no) { + for (var i : target.getGenericInterfaces()) { + if (i instanceof ParameterizedType) { + ParameterizedType p = (ParameterizedType) i; + if (p.getRawType().equals(TypeAliasMapper.class)) { + return p.getActualTypeArguments()[no]; + } else { + var result = extractTypeAliasParamImpl((Class) p.getRawType(), no); + if (result instanceof Class) { + return result; + } else if (result instanceof TypeVariable) { + var params = ((Class) p.getRawType()).getTypeParameters(); + TypeVariable tv = (TypeVariable) result; + for (var j = 0; j < params.length; j++) { + if (params[j].getName().equals((tv).getName())) { + return p.getActualTypeArguments()[j]; + } + } + return tv; + } + } + } else if (i instanceof Class) { + return extractTypeAliasParamImpl((Class) i, no); + } + } + throw new RuntimeException("Could not extract type params from " + target); + } + public static class ByteArraySerializer extends StdSerializer { public ByteArraySerializer() { @@ -67,4 +119,41 @@ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx } + public static class TypeAliasDeSerializer extends StdDeserializer { + + final TypeAliasMapper mapper; + final Class serializedType; + + public TypeAliasDeSerializer(Class type, Class serializedType, TypeAliasMapper mapper) { + super(type); + this.mapper = mapper; + this.serializedType = serializedType; + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + var s = ctxt.readValue(p, serializedType); + return mapper.decode(s); + } + + } + + public static class TypeAliasSerializer extends StdSerializer { + + final TypeAliasMapper mapper; + final Class serializedType; + + public TypeAliasSerializer(Class type, Class serializedType, TypeAliasMapper mapper) { + super(type); + this.mapper = mapper; + this.serializedType = serializedType; + } + + @Override + public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException { + var s = mapper.encode(value); + gen.writeObject(s); + } + } + } diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java index 50f6fac6f1..46351d4430 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java @@ -1,5 +1,7 @@ package xyz.block.ftl.runtime; +import java.nio.charset.StandardCharsets; + import jakarta.inject.Singleton; import io.grpc.stub.StreamObserver; @@ -18,8 +20,11 @@ public VerbHandler(VerbRegistry registry) { @Override public void call(CallRequest request, StreamObserver responseObserver) { + System.out.println("IN: " + request.getBody().toString(StandardCharsets.UTF_8)); try { var response = registry.invoke(request); + + System.out.println("OUT: " + response.getBody().toString(StandardCharsets.UTF_8)); responseObserver.onNext(response); responseObserver.onCompleted(); } catch (Exception e) { 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 new file mode 100644 index 0000000000..ea6f3f8f59 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/test/java/xyz/block/ftl/runtime/JsonSerializationConfigTest.java @@ -0,0 +1,72 @@ +package xyz.block.ftl.runtime; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import xyz.block.ftl.TypeAliasMapper; + +class JsonSerializationConfigTest { + + @Test + public void testExtraction() { + Assertions.assertEquals(AtomicInteger.class, + JsonSerializationConfig.extractTypeAliasParamImpl(FullIntImplementation.class, 0)); + Assertions.assertEquals(Integer.class, + JsonSerializationConfig.extractTypeAliasParamImpl(FullIntImplementation.class, 1)); + + Assertions.assertEquals(AtomicInteger.class, + JsonSerializationConfig.extractTypeAliasParamImpl(PartialIntImplementation.class, 0)); + Assertions.assertEquals(Integer.class, + JsonSerializationConfig.extractTypeAliasParamImpl(PartialIntImplementation.class, 1)); + + Assertions.assertEquals(AtomicInteger.class, + JsonSerializationConfig.extractTypeAliasParamImpl(AtomicIntTypeMapping.class, 0)); + Assertions.assertEquals(Integer.class, + JsonSerializationConfig.extractTypeAliasParamImpl(AtomicIntTypeMapping.class, 1)); + } + + public static class AtomicIntTypeMapping implements TypeAliasMapper { + @Override + public Integer encode(AtomicInteger object) { + return object.get(); + } + + @Override + public AtomicInteger decode(Integer serialized) { + return new AtomicInteger(serialized); + } + } + + public static interface PartialIntMapper extends TypeAliasMapper { + } + + public static class PartialIntImplementation implements PartialIntMapper { + @Override + public Integer encode(AtomicInteger object) { + return object.get(); + } + + @Override + public AtomicInteger decode(Integer serialized) { + return new AtomicInteger(serialized); + } + } + + public static interface FullIntMapper extends TypeAliasMapper { + } + + public static class FullIntImplementation implements FullIntMapper { + @Override + public Integer encode(AtomicInteger object) { + return object.get(); + } + + @Override + public AtomicInteger decode(Integer serialized) { + return new AtomicInteger(serialized); + } + } + +} 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 1ea2f65aa8..c332343bdd 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 @@ -6,6 +6,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -26,6 +27,8 @@ import xyz.block.ftl.GeneratedRef; import xyz.block.ftl.Subscription; +import xyz.block.ftl.TypeAlias; +import xyz.block.ftl.TypeAliasMapper; import xyz.block.ftl.VerbClient; import xyz.block.ftl.VerbClientDefinition; import xyz.block.ftl.VerbClientEmpty; @@ -44,8 +47,35 @@ public class JavaCodeGenerator extends JVMCodeGenerator { public static final String CLIENT = "Client"; public static final String PACKAGE_PREFIX = "ftl."; - protected void generateTopicSubscription(Module module, Topic data, String packageName, Map typeAliasMap, + @Override + protected void generateTypeAliasMapper(String module, String name, String packageName, Optional nativeTypeAlias, Path outputDir) throws IOException { + TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(className(name) + TYPE_MAPPER) + .addAnnotation(AnnotationSpec.builder(TypeAlias.class) + .addMember("name", "\"" + name + "\"") + .addMember("module", "\"" + module + "\"") + .build()) + .addModifiers(Modifier.PUBLIC); + if (nativeTypeAlias.isEmpty()) { + TypeVariableName finalType = TypeVariableName.get("T"); + typeBuilder.addTypeVariable(finalType); + typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(TypeAliasMapper.class), + finalType, ClassName.get(String.class))); + } else { + typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(TypeAliasMapper.class), + ClassName.bestGuess(nativeTypeAlias.get()), ClassName.get(String.class))); + } + TypeSpec theType = typeBuilder + .build(); + + JavaFile javaFile = JavaFile.builder(packageName, theType) + .build(); + + javaFile.writeTo(outputDir); + } + + protected void generateTopicSubscription(Module module, Topic data, String packageName, Map typeAliasMap, + Map nativeTypeAliasMap, Path outputDir) throws IOException { String thisType = className(data.getName() + "Subscription"); TypeSpec.Builder dataBuilder = TypeSpec.annotationBuilder(thisType) @@ -68,7 +98,8 @@ protected void generateTopicSubscription(Module module, Topic data, String packa javaFile.writeTo(outputDir); } - protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, Path outputDir) + protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, + Map nativeTypeAliasMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType) @@ -89,7 +120,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Path outputDir) throws IOException { + Map nativeTypeAliasMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType) .addAnnotation( @@ -106,7 +137,7 @@ protected void generateDataObject(Module module, Data data, String packageName, Map sortedFields = new TreeMap<>(); for (var i : data.getFieldsList()) { - TypeName dataType = toAnnotatedJavaTypeName(i.getType(), typeAliasMap); + TypeName dataType = toAnnotatedJavaTypeName(i.getType(), typeAliasMap, nativeTypeAliasMap); String name = i.getName(); var fieldName = toJavaName(name); dataBuilder.addField(dataType, fieldName, Modifier.PRIVATE); @@ -150,7 +181,8 @@ protected void generateDataObject(Module module, Data data, String packageName, javaFile.writeTo(outputDir); } - protected void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, Path outputDir) + protected void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, + Map nativeTypeAliasMap, Path outputDir) throws IOException { TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(className(verb.getName()) + CLIENT) .addAnnotation(AnnotationSpec.builder(VerbClientDefinition.class) @@ -162,23 +194,23 @@ protected void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap) { - var results = toJavaTypeName(type, typeAliasMap, false); + private TypeName toAnnotatedJavaTypeName(Type type, Map typeAliasMap, + Map nativeTypeAliasMap) { + var results = toJavaTypeName(type, typeAliasMap, nativeTypeAliasMap, false); if (type.hasRef() || type.hasArray() || type.hasBytes() || type.hasString() || type.hasMap() || type.hasTime()) { return results.annotated(AnnotationSpec.builder(NotNull.class).build()); } return results; } - private TypeName toJavaTypeName(Type type, Map typeAliasMap, boolean boxPrimitives) { + private TypeName toJavaTypeName(Type type, Map typeAliasMap, Map nativeTypeAliasMap, + boolean boxPrimitives) { if (type.hasArray()) { return ParameterizedTypeName.get(ClassName.get(List.class), - toJavaTypeName(type.getArray().getElement(), typeAliasMap, false)); + toJavaTypeName(type.getArray().getElement(), typeAliasMap, nativeTypeAliasMap, false)); } else if (type.hasString()) { return ClassName.get(String.class); } else if (type.hasOptional()) { // Always box for optional, as normal primities can't be null - return toJavaTypeName(type.getOptional().getType(), typeAliasMap, true); + return toJavaTypeName(type.getOptional().getType(), typeAliasMap, nativeTypeAliasMap, true); } else if (type.hasRef()) { if (type.getRef().getModule().isEmpty()) { return TypeVariableName.get(type.getRef().getName()); } DeclRef key = new DeclRef(type.getRef().getModule(), type.getRef().getName()); + if (nativeTypeAliasMap.containsKey(key)) { + return ClassName.bestGuess(nativeTypeAliasMap.get(key)); + } if (typeAliasMap.containsKey(key)) { - return toJavaTypeName(typeAliasMap.get(key), typeAliasMap, boxPrimitives); + return toJavaTypeName(typeAliasMap.get(key), typeAliasMap, nativeTypeAliasMap, boxPrimitives); } var params = type.getRef().getTypeParametersList(); ClassName className = ClassName.get(PACKAGE_PREFIX + type.getRef().getModule(), type.getRef().getName()); @@ -229,13 +266,14 @@ private TypeName toJavaTypeName(Type type, Map typeAliasMap, bool return className; } List javaTypes = params.stream() - .map(s -> s.hasUnit() ? WildcardTypeName.subtypeOf(Object.class) : toJavaTypeName(s, typeAliasMap, true)) + .map(s -> s.hasUnit() ? WildcardTypeName.subtypeOf(Object.class) + : toJavaTypeName(s, typeAliasMap, nativeTypeAliasMap, true)) .toList(); return ParameterizedTypeName.get(className, javaTypes.toArray(new TypeName[javaTypes.size()])); } else if (type.hasMap()) { return ParameterizedTypeName.get(ClassName.get(Map.class), - toJavaTypeName(type.getMap().getKey(), typeAliasMap, true), - toJavaTypeName(type.getMap().getValue(), typeAliasMap, true)); + toJavaTypeName(type.getMap().getKey(), typeAliasMap, nativeTypeAliasMap, true), + toJavaTypeName(type.getMap().getValue(), typeAliasMap, nativeTypeAliasMap, true)); } else if (type.hasTime()) { return ClassName.get(ZonedDateTime.class); } else if (type.hasInt()) { 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 a06d7d2fa5..038d8f164e 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 @@ -5,6 +5,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import com.squareup.kotlinpoet.AnnotationSpec; @@ -22,6 +23,8 @@ import xyz.block.ftl.GeneratedRef; import xyz.block.ftl.Subscription; +import xyz.block.ftl.TypeAlias; +import xyz.block.ftl.TypeAliasMapper; import xyz.block.ftl.VerbClient; import xyz.block.ftl.VerbClientDefinition; import xyz.block.ftl.VerbClientEmpty; @@ -40,8 +43,34 @@ public class KotlinCodeGenerator extends JVMCodeGenerator { public static final String CLIENT = "Client"; public static final String PACKAGE_PREFIX = "ftl."; - protected void generateTopicSubscription(Module module, Topic data, String packageName, Map typeAliasMap, + @Override + protected void generateTypeAliasMapper(String module, String name, String packageName, Optional nativeTypeAlias, Path outputDir) throws IOException { + String thisType = className(name) + TYPE_MAPPER; + TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(thisType) + .addAnnotation(AnnotationSpec.builder(TypeAlias.class) + .addMember("name=\"" + name + "\"") + .addMember("module=\"" + module + "\"") + .build()) + .addModifiers(KModifier.PUBLIC); + if (nativeTypeAlias.isEmpty()) { + TypeVariableName finalType = TypeVariableName.get("T"); + typeBuilder.addTypeVariable(finalType); + typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.bestGuess(TypeAliasMapper.class.getName()), + finalType, new ClassName("kotlin", "String")), CodeBlock.of("")); + } else { + typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.bestGuess(TypeAliasMapper.class.getName()), + ClassName.bestGuess(nativeTypeAlias.get()), new ClassName("kotlin", "String")), CodeBlock.of("")); + } + + FileSpec javaFile = FileSpec.builder(packageName, thisType) + .addType(typeBuilder.build()) + .build(); + javaFile.writeTo(outputDir); + } + + protected void generateTopicSubscription(Module module, Topic data, String packageName, Map typeAliasMap, + Map nativeTypeAliasMap, Path outputDir) throws IOException { String thisType = className(data.getName() + "Subscription"); TypeSpec.Builder dataBuilder = TypeSpec.annotationBuilder(ClassName.bestGuess(thisType)); @@ -63,7 +92,8 @@ protected void generateTopicSubscription(Module module, Topic data, String packa javaFile.writeTo(outputDir); } - protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, Path outputDir) + protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, + Map nativeTypeAliasMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType) @@ -84,7 +114,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap, - Path outputDir) throws IOException { + Map nativeTypeAliasMap, Path outputDir) throws IOException { String thisType = className(data.getName()); TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType) .addAnnotation( @@ -102,7 +132,7 @@ protected void generateDataObject(Module module, Data data, String packageName, FunSpec.Builder constructorBuilder = FunSpec.constructorBuilder(); for (var i : data.getFieldsList()) { - TypeName dataType = toKotlinTypeName(i.getType(), typeAliasMap); + TypeName dataType = toKotlinTypeName(i.getType(), typeAliasMap, nativeTypeAliasMap); String name = i.getName(); var fieldName = toJavaName(name); constructorBuilder.addParameter(fieldName, dataType); @@ -118,7 +148,8 @@ protected void generateDataObject(Module module, Data data, String packageName, javaFile.writeTo(outputDir); } - protected void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, Path outputDir) + protected void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap, + Map nativeTypeAliasMap, Path outputDir) throws IOException { String thisType = className(verb.getName()) + CLIENT; TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(thisType) @@ -131,23 +162,23 @@ protected void generateVerb(Module module, Verb verb, String packageName, Map clazz) { return new ClassName(clazz.getPackage().getName(), clazz.getSimpleName()); } - private TypeName toKotlinTypeName(Type type, Map typeAliasMap) { + private TypeName toKotlinTypeName(Type type, Map typeAliasMap, Map nativeTypeAliasMap) { if (type.hasArray()) { return ParameterizedTypeName.get(new ClassName("kotlin.collections", "List"), - toKotlinTypeName(type.getArray().getElement(), typeAliasMap)); + toKotlinTypeName(type.getArray().getElement(), typeAliasMap, nativeTypeAliasMap)); } else if (type.hasString()) { return new ClassName("kotlin", "String"); } else if (type.hasOptional()) { // Always box for optional, as normal primities can't be null - return toKotlinTypeName(type.getOptional().getType(), typeAliasMap); + return toKotlinTypeName(type.getOptional().getType(), typeAliasMap, nativeTypeAliasMap); } else if (type.hasRef()) { if (type.getRef().getModule().isEmpty()) { return TypeVariableName.get(type.getRef().getName()); } DeclRef key = new DeclRef(type.getRef().getModule(), type.getRef().getName()); + if (nativeTypeAliasMap.containsKey(key)) { + String className = nativeTypeAliasMap.get(key); + var idx = className.lastIndexOf('.'); + if (idx != -1) { + return new ClassName(className.substring(0, idx), className.substring(idx + 1)); + } + return new ClassName("", className); + } if (typeAliasMap.containsKey(key)) { - return toKotlinTypeName(typeAliasMap.get(key), typeAliasMap); + return toKotlinTypeName(typeAliasMap.get(key), typeAliasMap, nativeTypeAliasMap); } var params = type.getRef().getTypeParametersList(); ClassName className = new ClassName(PACKAGE_PREFIX + type.getRef().getModule(), type.getRef().getName()); @@ -196,13 +235,13 @@ private TypeName toKotlinTypeName(Type type, Map typeAliasMap) { } List javaTypes = params.stream() .map(s -> s.hasUnit() ? WildcardTypeName.consumerOf(new ClassName("kotlin", "Any")) - : toKotlinTypeName(s, typeAliasMap)) + : toKotlinTypeName(s, typeAliasMap, nativeTypeAliasMap)) .toList(); return ParameterizedTypeName.get(className, javaTypes.toArray(new TypeName[javaTypes.size()])); } else if (type.hasMap()) { return ParameterizedTypeName.get(new ClassName("kotlin.collections", "Map"), - toKotlinTypeName(type.getMap().getKey(), typeAliasMap), - toKotlinTypeName(type.getMap().getValue(), typeAliasMap)); + toKotlinTypeName(type.getMap().getKey(), typeAliasMap, nativeTypeAliasMap), + toKotlinTypeName(type.getMap().getValue(), typeAliasMap, nativeTypeAliasMap)); } else if (type.hasTime()) { return className(ZonedDateTime.class); } else if (type.hasInt()) { diff --git a/jvm-runtime/jvm_integration_test.go b/jvm-runtime/jvm_integration_test.go index 7051317cac..c898528781 100644 --- a/jvm-runtime/jvm_integration_test.go +++ b/jvm-runtime/jvm_integration_test.go @@ -8,6 +8,8 @@ import ( "github.com/alecthomas/assert/v2" + "github.com/tbd54566975/web5-go/dids/did" + "github.com/TBD54566975/ftl/go-runtime/ftl" in "github.com/TBD54566975/ftl/internal/integration" @@ -65,6 +67,11 @@ func TestJVMToGoCall(t *testing.T) { ArrayField: ftl.Some[[]string]([]string{"foo", "bar"}), MapField: ftl.Some[map[string]string](map[string]string{"gar": "har"}), } + exampleDid := did.DID{ + Method: "web", + ID: "abc123", + URI: "did:web:abc123", + } tests := []in.SubTest{} tests = append(tests, PairedTest("emptyVerb", func(module string) in.Action { return in.Call(module, "emptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) { @@ -122,6 +129,7 @@ func TestJVMToGoCall(t *testing.T) { tests = append(tests, PairedVerbTest("optionalTestObjectVerb", exampleObject)...) tests = append(tests, PairedVerbTest("optionalTestObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...) + tests = append(tests, PairedVerbTest("externalTypeVerb", exampleDid)...) // 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]())...) diff --git a/jvm-runtime/testdata/go/gomodule/go.mod b/jvm-runtime/testdata/go/gomodule/go.mod index bc29756169..9796f3035c 100644 --- a/jvm-runtime/testdata/go/gomodule/go.mod +++ b/jvm-runtime/testdata/go/gomodule/go.mod @@ -4,7 +4,10 @@ go 1.23.0 replace github.com/TBD54566975/ftl => ./../../../.. -require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 +require ( + github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 + github.com/tbd54566975/web5-go v0.24.0 +) require ( connectrpc.com/connect v1.16.2 // indirect @@ -18,6 +21,7 @@ require ( github.com/benbjohnson/clock v1.3.5 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/jvm-runtime/testdata/go/gomodule/go.sum b/jvm-runtime/testdata/go/gomodule/go.sum index e0128d507c..2bb0537fa2 100644 --- a/jvm-runtime/testdata/go/gomodule/go.sum +++ b/jvm-runtime/testdata/go/gomodule/go.sum @@ -6,6 +6,7 @@ connectrpc.com/otelconnect v0.7.1 h1:scO5pOb0i4yUE66CnNrHeK1x51yq0bE0ehPg6WvzXJY connectrpc.com/otelconnect v0.7.1/go.mod h1:dh3bFgHBTb2bkqGCeVVOtHJreSns7uu9wwL2Tbz17ms= github.com/TBD54566975/scaffolder v1.1.0 h1:R92zjC4XiS/lGCxJ8Ebn93g8gC0LU9qo06AAKo9cEJE= github.com/TBD54566975/scaffolder v1.1.0/go.mod h1:dRi67GryEhZ5u0XRSiR294SYaqAfnCkZ7u3rmc4W6iI= +github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= @@ -33,6 +34,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -97,6 +102,10 @@ github.com/swaggest/jsonschema-go v0.3.72 h1:IHaGlR1bdBUBPfhe4tfacN2TGAPKENEGiNy github.com/swaggest/jsonschema-go v0.3.72/go.mod h1:OrGyEoVqpfSFJ4Am4V/FQcQ3mlEC1vVeleA+5ggbVW4= github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/tbd54566975/web5-go v0.24.0 h1:21pLJCeMRt1hEdedzu3FpSJLsnk+MZ2SOxKYQxj3gC8= +github.com/tbd54566975/web5-go v0.24.0/go.mod h1:7M5TGxGZY4g/eh0aoSD1h6AGr5yJXAc706wSquc8QsE= +github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa h1:2EwhXkNkeMjX9iFYGWLPQLPhw9O58BhnYgtYKeqybcY= +github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa/go.mod h1:is48sjgBanWcA5CQrPBu9Y5yABY/T2awj/zI65bq704= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= diff --git a/jvm-runtime/testdata/go/gomodule/server.go b/jvm-runtime/testdata/go/gomodule/server.go index 9adc032019..9453fe6ca4 100644 --- a/jvm-runtime/testdata/go/gomodule/server.go +++ b/jvm-runtime/testdata/go/gomodule/server.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + "github.com/tbd54566975/web5-go/dids/did" + "github.com/TBD54566975/ftl/go-runtime/ftl" ) @@ -30,6 +32,10 @@ type TestObjectOptionalFields struct { MapField ftl.Option[map[string]string] } +//ftl:typealias +//ftl:typemap kotlin "web5.sdk.dids.didcore.Did" +type DID = did.DID + // Test different signatures //ftl:verb export @@ -37,6 +43,11 @@ func SourceVerb(ctx context.Context) (string, error) { return "Source Verb", nil } +type ExportedType[T any, S any] interface { + FTLEncode(d T) (S, error) + FTLDecode(in S) (T, error) +} + //ftl:verb export func SinkVerb(ctx context.Context, req string) error { return nil @@ -155,3 +166,8 @@ func OptionalTestObjectVerb(ctx context.Context, val ftl.Option[TestObject]) (ft func OptionalTestObjectOptionalFieldsVerb(ctx context.Context, val ftl.Option[TestObjectOptionalFields]) (ftl.Option[TestObjectOptionalFields], error) { return val, nil } + +//ftl:verb export +func ExternalTypeVerb(ctx context.Context, did DID) (DID, error) { + return did, nil +} diff --git a/jvm-runtime/testdata/java/passthrough/pom.xml b/jvm-runtime/testdata/java/passthrough/pom.xml index 49218c0d72..889298898f 100644 --- a/jvm-runtime/testdata/java/passthrough/pom.xml +++ b/jvm-runtime/testdata/java/passthrough/pom.xml @@ -13,12 +13,9 @@ - io.quarkus - quarkus-hibernate-orm - - - io.quarkus - quarkus-jdbc-postgresql + xyz.block + web5-dids + 2.0.1-debug1 diff --git a/jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/DidMapper.java b/jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/DidMapper.java new file mode 100644 index 0000000000..15afd90002 --- /dev/null +++ b/jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/DidMapper.java @@ -0,0 +1,19 @@ +package xyz.block.ftl.test; + +import java.nio.charset.StandardCharsets; + +import ftl.gomodule.DidTypeAliasMapper; +import web5.sdk.dids.didcore.Did; + +public class DidMapper implements DidTypeAliasMapper { + + @Override + public Did decode(String bytes) { + return Did.Parser.parse(bytes); + } + + @Override + public String encode(Did did) { + return new String(did.marshalText(), StandardCharsets.UTF_8); + } +} diff --git a/jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index 65a7573565..34e78d9792 100644 --- a/jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/passthrough/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -10,6 +10,7 @@ import ftl.gomodule.BytesVerbClient; import ftl.gomodule.EmptyVerbClient; import ftl.gomodule.ErrorEmptyVerbClient; +import ftl.gomodule.ExternalTypeVerbClient; import ftl.gomodule.FloatVerbClient; import ftl.gomodule.IntVerbClient; import ftl.gomodule.OptionalBoolVerbClient; @@ -32,6 +33,7 @@ import ftl.gomodule.TestObjectOptionalFieldsVerbClient; import ftl.gomodule.TestObjectVerbClient; import ftl.gomodule.TimeVerbClient; +import web5.sdk.dids.didcore.Did; import xyz.block.ftl.Export; import xyz.block.ftl.Verb; @@ -185,4 +187,10 @@ public TestObjectOptionalFields optionalTestObjectOptionalFieldsVerb(TestObjectO return client.call(val); } + @Export + @Verb + public Did externalTypeVerb(Did val, ExternalTypeVerbClient client) { + return client.call(val); + } + } diff --git a/jvm-runtime/testdata/kotlin/passthrough/pom.xml b/jvm-runtime/testdata/kotlin/passthrough/pom.xml index c641ddb2cb..a00305c923 100644 --- a/jvm-runtime/testdata/kotlin/passthrough/pom.xml +++ b/jvm-runtime/testdata/kotlin/passthrough/pom.xml @@ -11,6 +11,14 @@ 1.0-SNAPSHOT + + + xyz.block + web5-dids + 2.0.1-debug1 + + + diff --git a/jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/DidMapper.kt b/jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/DidMapper.kt new file mode 100644 index 0000000000..2aac8c981a --- /dev/null +++ b/jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/DidMapper.kt @@ -0,0 +1,18 @@ +package xyz.block.ftl.test + +import ftl.gomodule.DidTypeAliasMapper +import web5.sdk.dids.didcore.Did + + +class DidMapper : DidTypeAliasMapper +{ + + override fun decode(bytes: String): Did { + return Did.Parser.parse(bytes) + } + + override fun encode(did: Did): String { + return String(did.marshalText()) + } + +} diff --git a/jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt b/jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt index f783ebc8a7..d9010f65d3 100644 --- a/jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt +++ b/jvm-runtime/testdata/kotlin/passthrough/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt @@ -1,6 +1,7 @@ package xyz.block.ftl.test import ftl.gomodule.* +import web5.sdk.dids.didcore.Did import xyz.block.ftl.Export import xyz.block.ftl.Verb import java.time.ZonedDateTime @@ -155,3 +156,9 @@ fun optionalTestObjectOptionalFieldsVerb( ): TestObjectOptionalFields { return client.call(payload!!) } + +@Export +@Verb +fun externalTypeVerb(did: Did, client: ExternalTypeVerbClient): Did { + return client.call(did) +}