diff --git a/docs/content/docs/reference/externaltypes.md b/docs/content/docs/reference/externaltypes.md index 5599171ebd..e70dba5578 100644 --- a/docs/content/docs/reference/externaltypes.md +++ b/docs/content/docs/reference/externaltypes.md @@ -15,8 +15,20 @@ top = false ## Using external types +FTL supports the use of external types in your FTL modules. External types are types defined in other packages or modules that are not part of the FTL module. + +The primary difference is that external types are not defined in the FTL schema, and therefore serialization and deserialization of these types is not handled +by FTL. Instead, FTL relies on the runtime to handle serialization and deserialization of these types. + +In some cases this feature can also be used to provide custom serialization and deserialization logic for types that are not directly supported by FTL, even +if they are defined in the same package as the FTL module. + To use an external type in your FTL module schema, declare a type alias over the external type: +{% code_selector() %} + + + ```go //ftl:typealias type FtlType external.OtherType @@ -25,7 +37,7 @@ type FtlType external.OtherType type FtlType2 = external.OtherType ``` -The external type is widened to `Any` in the FTL schema, and the corresponding type alias will include metadata +The external type is widened to `Any` in the FTL schema, and the corresponding type alias will include metadata for the runtime-specific type mapping: ``` @@ -33,29 +45,157 @@ typealias FtlType Any +typemap go "github.com/external.OtherType" ``` -Users can achieve functionally equivalent behavior to using the external type directly by using the declared -alias (`FtlType`) in place of the external type in any other schema declarations (e.g. as the type of a Verb request). Direct usage of the external type in schema declarations is not supported; + + +```kotlin +@TypeAlias(name = "OtherType") +class OtherTypeTypeMapper : TypeAliasMapper { + override fun encode(`object`: OtherType): JsonNode { + return TextNode.valueOf(`object`.value) + } + + override fun decode(serialized: JsonNode): OtherType { + if (serialized.isTextual) { + return OtherType(serialized.textValue()) + } + throw RuntimeException("Expected a textual value") + } +} +``` + +In the example above the external type is widened to `Any` in the FTL schema, and the corresponding type alias will include metadata +for the runtime-specific type mapping: + +``` +typealias FtlType Any + +typemap java "foo.bar.OtherType" +``` + +Note that for JVM languages `java` is always used as the runtime name, regardless of the actual language used. + +It is also possible to map to any other valid FTL type (e.g. `String`) by use this as the second type parameter: + +Users can achieve functionally equivalent behavior to using the external type directly by using the declared +alias (`FtlType`) in place of the external type in any other schema declarations (e.g. as the type of a Verb request). Direct usage of the external type in schema declarations is not supported; instead, the type alias must be used. +```kotlin +@TypeAlias(name = "OtherType") +class OtherTypeTypeMapper : TypeAliasMapper { + override fun encode(other: OtherType): JsonNode { + return other.value + } + + override fun decode(serialized: String): OtherType { + return OtherType(serialized.textValue()) + } +} +``` + +The corresponding type alias will be to a `String`, which makes the schema more useful: + +``` +typealias FtlType String + +typemap kotlin "foo.bar.OtherType" +``` + + +```java +@TypeAlias(name = "OtherType") +public class OtherTypeTypeMapper implements TypeAliasMapper { + @Override + public JsonNode encode(OtherType object) { + return TextNode.valueOf(object.getValue()); + } + + @Override + public AnySerializedType decode(OtherType serialized) { + if (serialized.isTextual()) { + return new OtherType(serialized.textValue()); + } + throw new RuntimeException("Expected a textual value"); + } +} +``` + +In the example above the external type is widened to `Any` in the FTL schema, and the corresponding type alias will include metadata +for the runtime-specific type mapping: + +``` +typealias FtlType Any + +typemap java "foo.bar.OtherType" +``` + +It is also possible to map to any other valid FTL type (e.g. `String`) by use this as the second type parameter: + + +```java +@TypeAlias(name = "OtherType") +public class OtherTypeTypeMapper implements TypeAliasMapper { + @Override + public String encode(OtherType object) { + return object.getValue(); + } + + @Override + public String decode(OtherType serialized) { + return new OtherType(serialized.textValue()); + } +} +``` + +The corresponding type alias will be to a `String`, which makes the schema more useful: + +``` +typealias FtlType String + +typemap java "com.external.other.OtherType" +``` +{% end %} + + FTL will automatically serialize and deserialize the external type to the strong type indicated by the mapping. ## Cross-Runtime Type Mappings -FTL also provides the capability to declare type mappings for other runtimes. For instance, to include a type mapping for Kotlin, you can +FTL also provides the capability to declare type mappings for other runtimes. For instance, to include a type mapping for another language, you can annotate your type alias declaration as follows: + +{% code_selector() %} + + ```go //ftl:typealias -//ftl:typemap kotlin "com.external.other.OtherType" +//ftl:typemap java "com.external.other.OtherType" type FtlType external.OtherType ``` + + +```kotlin +@TypeAlias( + name = "OtherType", + languageTypeMappings = [LanguageTypeMapping(language = "go", type = "github.com/external.OtherType")] +) +``` + + + +```java +@TypeAlias(name = "OtherType", languageTypeMappings = { + @LanguageTypeMapping(language = "go", type = "github.com/external.OtherType"), +}) +... +``` + +{% end %} + In the FTL schema, this will appear as: ``` typealias FtlType Any +typemap go "github.com/external.OtherType" - +typemap kotlin "com.external.other.OtherType" + +typemap java "com.external.other.OtherType" ``` This allows FTL to decode the type properly in other languages, for seamless diff --git a/internal/integration/actions.go b/internal/integration/actions.go index 34e6f501ce..13b3ba4e5a 100644 --- a/internal/integration/actions.go +++ b/internal/integration/actions.go @@ -418,7 +418,7 @@ func VerifySchema(check func(ctx context.Context, t testing.TB, sch *schemapb.Sc } // VerifySchemaVerb lets you test the current schema for a specific verb -func VerifySchemaVerb(module string, verb string, check func(ctx context.Context, t testing.TB, sch *schemapb.Verb)) Action { +func VerifySchemaVerb(module string, verb string, check func(ctx context.Context, t testing.TB, schema *schemapb.Schema, verb *schemapb.Verb)) Action { return func(t testing.TB, ic TestContext) { sch, err := ic.Controller.GetSchema(ic, connect.NewRequest(&ftlv1.GetSchemaRequest{})) if err != nil { @@ -433,13 +433,13 @@ func VerifySchemaVerb(module string, verb string, check func(ctx context.Context if m.Name == module { for _, v := range m.Decls { if v.GetVerb() != nil && v.GetVerb().Name == verb { - check(ic.Context, t, v.GetVerb()) + check(ic.Context, t, sch.Msg.GetSchema(), v.GetVerb()) return } } } - } + t.Errorf("verb %s.%s not found in schema", module, verb) } } diff --git a/internal/schema/schema.go b/internal/schema/schema.go index 5ef240a4be..14081e8912 100644 --- a/internal/schema/schema.go +++ b/internal/schema/schema.go @@ -61,7 +61,7 @@ func (s *Schema) ResolveRequestResponseType(ref *Ref) (Symbol, error) { } } - return s.resolveToDataMonomorphised(ref, nil) + return s.resolveToSymbolMonomorphised(ref, nil) } // ResolveMonomorphised resolves a reference to a monomorphised Data type. @@ -93,6 +93,29 @@ func (s *Schema) resolveToDataMonomorphised(n Node, parent Node) (*Data, error) } } +func (s *Schema) resolveToSymbolMonomorphised(n Node, parent Node) (Symbol, error) { + switch typ := n.(type) { + case *Ref: + resolved, ok := s.Resolve(typ).Get() + if !ok { + return nil, fmt.Errorf("unknown ref %s", typ) + } + return s.resolveToSymbolMonomorphised(resolved, typ) + case *Data: + p, ok := parent.(*Ref) + if !ok { + return nil, fmt.Errorf("expected data node parent to be a ref, got %T", p) + } + return typ.Monomorphise(p) + case *TypeAlias: + return s.resolveToSymbolMonomorphised(typ.Type, typ) + case Symbol: + return typ, nil + default: + return nil, fmt.Errorf("expected data or type alias of data, got %T", typ) + } +} + // ResolveWithModule a reference to a declaration and its module. func (s *Schema) ResolveWithModule(ref *Ref) (optional.Option[Decl], optional.Option[*Module]) { for _, module := range s.Modules { 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..b88cd68f14 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 @@ -61,13 +61,18 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { 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; + String nativeName = md.getTypeMap().getNativeName(); + var existing = getClass().getClassLoader() + .getResource(nativeName.replace(".", "/") + ".class"); + if (existing != null) { + nativeTypeAliasMap.put(new DeclRef(module.getName(), data.getName()), + nativeName); + generateTypeAliasMapper(module.getName(), data.getName(), packageName, + Optional.of(nativeName), + context.outDir()); + handled = true; + break; + } } } } 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 6a14d13aa8..3606c8bb9c 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 @@ -524,15 +524,22 @@ public void writeTo(OutputStream out) throws IOException { moduleBuilder.build().writeTo(out); } - public void registerTypeAlias(String name, org.jboss.jandex.Type finalT, org.jboss.jandex.Type finalS, boolean exported) { + public void registerTypeAlias(String name, org.jboss.jandex.Type finalT, org.jboss.jandex.Type finalS, boolean exported, + Map languageMappings) { validateName(finalT.name().toString(), name); + TypeAlias.Builder typeAlias = TypeAlias.newBuilder().setType(buildType(finalS, exported, Nullability.NOT_NULL)) + .setName(name) + .addMetadata(Metadata + .newBuilder() + .setTypeMap(MetadataTypeMap.newBuilder().setRuntime("java").setNativeName(finalT.toString()) + .build()) + .build()); + for (var entry : languageMappings.entrySet()) { + typeAlias.addMetadata(Metadata.newBuilder().setTypeMap(MetadataTypeMap.newBuilder().setRuntime(entry.getKey()) + .setNativeName(entry.getValue()).build()).build()); + } moduleBuilder.addDecls(Decl.newBuilder() - .setTypeAlias(TypeAlias.newBuilder().setType(buildType(finalS, exported, Nullability.NOT_NULL)).setName(name) - .addMetadata(Metadata - .newBuilder() - .setTypeMap(MetadataTypeMap.newBuilder().setRuntime("java").setNativeName(finalT.toString()) - .build()) - .build())) + .setTypeAlias(typeAlias) .build()); } 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..9645af17a5 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,5 +1,9 @@ package xyz.block.ftl.deployment; +import java.util.HashMap; +import java.util.Map; + +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.Type; import org.jboss.jandex.TypeVariable; @@ -69,8 +73,16 @@ public void processTypeAlias(CombinedIndexBuildItem index, String name = mapper.value("name").asString(); typeAliasBuildItemBuildProducer.produce(new TypeAliasBuildItem(name, module, t, s, exported)); if (module.isEmpty()) { + Map languageMappings = new HashMap<>(); + AnnotationValue languageTypeMappingsValue = mapper.value("languageTypeMappings"); + if (languageTypeMappingsValue != null) { + for (var lang : languageTypeMappingsValue.asArrayList()) { + languageMappings.put(lang.asNested().value("language").asString(), + lang.asNested().value("type").asString()); + } + } schemaContributorBuildItemBuildProducer.produce(new SchemaContributorBuildItem(moduleBuilder -> moduleBuilder - .registerTypeAlias(name, finalT, finalS, exported))); + .registerTypeAlias(name, finalT, finalS, exported, languageMappings))); } } diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/LanguageTypeMapping.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/LanguageTypeMapping.java new file mode 100644 index 0000000000..e03b9bbdce --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/LanguageTypeMapping.java @@ -0,0 +1,14 @@ +package xyz.block.ftl; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({}) +public @interface LanguageTypeMapping { + + String language(); + + String type(); +} 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 index 1eac64675f..c52182dd41 100644 --- 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 @@ -12,4 +12,6 @@ String name(); String module() default ""; + + LanguageTypeMapping[] languageTypeMappings() default {}; } diff --git a/jvm-runtime/jvm_integration_test.go b/jvm-runtime/jvm_integration_test.go index fa6e76b3c4..71f5095b0a 100644 --- a/jvm-runtime/jvm_integration_test.go +++ b/jvm-runtime/jvm_integration_test.go @@ -10,11 +10,12 @@ import ( "github.com/alecthomas/assert/v2" + "github.com/alecthomas/repr" + schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema" "github.com/TBD54566975/ftl/go-runtime/ftl" in "github.com/TBD54566975/ftl/internal/integration" - - "github.com/alecthomas/repr" + "github.com/TBD54566975/ftl/internal/schema" ) func TestLifecycleJVM(t *testing.T) { @@ -147,9 +148,60 @@ func TestJVMCoreFunctionality(t *testing.T) { // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalTestObjectVerb", ftl.None[any]())...) // tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalTestObjectOptionalFieldsVerb", ftl.None[any]())...) + // Test custom serialized type mapped to a string + tests = append(tests, JVMTest("stringAliasedTypeSchema", func(name string, module string) in.Action { + return in.VerifySchemaVerb(module, "stringAliasedType", func(ctx context.Context, t testing.TB, sch *schemapb.Schema, verb *schemapb.Verb) { + assert.True(t, verb.Response.GetRef() != nil, "response was not a ref") + assert.True(t, verb.Request.GetRef() != nil, "request was not a ref") + fullSchema, err := schema.FromProto(sch) + assert.NoError(t, err, "failed to convert schema") + req := fullSchema.Resolve(schema.RefFromProto(verb.Request.GetRef())) + assert.True(t, req.Ok(), "request not found") + if typeAlias, ok := req.MustGet().(*schema.TypeAlias); ok { + if _, ok := typeAlias.Type.(*schema.String); !ok { + assert.False(t, true, "request type alias not a string") + } + } else { + assert.False(t, true, "request not a type alias") + } + }) + })...) + // Test custom serialized type mapped to an any + tests = append(tests, JVMTest("anyAliasedTypeSchema", func(name string, module string) in.Action { + return in.VerifySchemaVerb(module, "anyAliasedType", func(ctx context.Context, t testing.TB, sch *schemapb.Schema, verb *schemapb.Verb) { + assert.True(t, verb.Response.GetRef() != nil, "response was not a ref") + assert.True(t, verb.Request.GetRef() != nil, "request was not a ref") + fullSchema, err := schema.FromProto(sch) + assert.NoError(t, err, "failed to convert schema") + req := fullSchema.Resolve(schema.RefFromProto(verb.Request.GetRef())) + assert.True(t, req.Ok(), "request not found") + if typeAlias, ok := req.MustGet().(*schema.TypeAlias); ok { + if _, ok := typeAlias.Type.(*schema.Any); !ok { + assert.False(t, true, "request type alias not a any") + } + goMap := "" + javaMap := "false" + for _, md := range typeAlias.Metadata { + if md, ok := md.(*schema.MetadataTypeMap); ok { + switch md.Runtime { + case "go": + goMap = md.NativeName + case "java": + javaMap = md.NativeName + } + } + } + assert.Equal(t, "github.com/blockxyz/ftl/test.AnySerializedType", goMap, "go language map not found") + assert.Equal(t, "xyz.block.ftl.test.AnySerializedType", javaMap, "Java language map not found") + } else { + assert.False(t, true, "request not a type alias") + } + + }) + })...) // Schema comments tests = append(tests, JVMTest("schemaComments", func(name string, module string) in.Action { - return in.VerifySchemaVerb(module, "emptyVerb", func(ctx context.Context, t testing.TB, verb *schemapb.Verb) { + return in.VerifySchemaVerb(module, "emptyVerb", func(ctx context.Context, t testing.TB, schema *schemapb.Schema, verb *schemapb.Verb) { ok := false for _, comment := range verb.GetComments() { if strings.Contains(comment, "JAVA COMMENT") { @@ -282,14 +334,14 @@ func subTest(name string, test in.Action) in.Action { } func verifyOptionalVerb(name string, module string) in.Action { - return in.VerifySchemaVerb(module, name, func(ctx context.Context, t testing.TB, verb *schemapb.Verb) { + return in.VerifySchemaVerb(module, name, func(ctx context.Context, t testing.TB, schema *schemapb.Schema, verb *schemapb.Verb) { assert.True(t, verb.Response.GetOptional() != nil, "response not optional") assert.True(t, verb.Request.GetOptional() != nil, "request not optional") }) } func verifyNonOptionalVerb(name string, module string) in.Action { - return in.VerifySchemaVerb(module, name, func(ctx context.Context, t testing.TB, verb *schemapb.Verb) { + return in.VerifySchemaVerb(module, name, func(ctx context.Context, t testing.TB, schema *schemapb.Schema, verb *schemapb.Verb) { assert.True(t, verb.Response.GetOptional() == nil, "response was optional") assert.True(t, verb.Request.GetOptional() == nil, "request was optional") }) diff --git a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/AnySerializedType.java b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/AnySerializedType.java new file mode 100644 index 0000000000..dff31f8616 --- /dev/null +++ b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/AnySerializedType.java @@ -0,0 +1,14 @@ +package xyz.block.ftl.test; + +public class AnySerializedType { + + private final String value; + + public AnySerializedType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/AnySerializedTypeMapper.java b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/AnySerializedTypeMapper.java new file mode 100644 index 0000000000..b1f0c9ebe1 --- /dev/null +++ b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/AnySerializedTypeMapper.java @@ -0,0 +1,26 @@ +package xyz.block.ftl.test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import xyz.block.ftl.LanguageTypeMapping; +import xyz.block.ftl.TypeAlias; +import xyz.block.ftl.TypeAliasMapper; + +@TypeAlias(name = "AnySerializedType", languageTypeMappings = { + @LanguageTypeMapping(language = "go", type = "github.com/blockxyz/ftl/test.AnySerializedType"), +}) +public class AnySerializedTypeMapper implements TypeAliasMapper { + @Override + public JsonNode encode(AnySerializedType object) { + return TextNode.valueOf(object.getValue()); + } + + @Override + public AnySerializedType decode(JsonNode serialized) { + if (serialized.isTextual()) { + return new AnySerializedType(serialized.textValue()); + } + throw new RuntimeException("Expected a textual value"); + } +} diff --git a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/CustomSerializedType.java b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/CustomSerializedType.java new file mode 100644 index 0000000000..f6e5d74abd --- /dev/null +++ b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/CustomSerializedType.java @@ -0,0 +1,14 @@ +package xyz.block.ftl.test; + +public class CustomSerializedType { + + private final String value; + + public CustomSerializedType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/CustomSerializedTypeMapper.java b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/CustomSerializedTypeMapper.java new file mode 100644 index 0000000000..567053a3d5 --- /dev/null +++ b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/CustomSerializedTypeMapper.java @@ -0,0 +1,17 @@ +package xyz.block.ftl.test; + +import xyz.block.ftl.TypeAlias; +import xyz.block.ftl.TypeAliasMapper; + +@TypeAlias(name = "CustomSerializedType") +public class CustomSerializedTypeMapper implements TypeAliasMapper { + @Override + public String encode(CustomSerializedType object) { + return object.getValue(); + } + + @Override + public CustomSerializedType decode(String serialized) { + return new CustomSerializedType(serialized); + } +} diff --git a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java index 4c785994e3..d7c5130285 100644 --- a/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java +++ b/jvm-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/test/TestInvokeGoFromJava.java @@ -191,7 +191,8 @@ public Boolean optionalBoolVerb(Boolean val, OptionalBoolVerbClient client) { @Export @Verb - public @Nullable Map optionalStringMapVerb(@Nullable Map val, OptionalStringMapVerbClient client) { + public @Nullable Map optionalStringMapVerb(@Nullable Map val, + OptionalStringMapVerbClient client) { return client.call(val); } @@ -220,4 +221,15 @@ public Did externalTypeVerb(Did val, ExternalTypeVerbClient client) { return client.call(val); } + @Export + @Verb + public CustomSerializedType stringAliasedType(CustomSerializedType type) { + return type; + } + + @Export + @Verb + public AnySerializedType anyAliasedType(AnySerializedType type) { + return type; + } } diff --git a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/AnySerializedType.kt b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/AnySerializedType.kt new file mode 100644 index 0000000000..debfe113c6 --- /dev/null +++ b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/AnySerializedType.kt @@ -0,0 +1,3 @@ +package xyz.block.ftl.test + +class AnySerializedType(val value: String) diff --git a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/AnySerializedTypeMapper.kt b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/AnySerializedTypeMapper.kt new file mode 100644 index 0000000000..42a1031df2 --- /dev/null +++ b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/AnySerializedTypeMapper.kt @@ -0,0 +1,24 @@ +package xyz.block.ftl.test + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.TextNode +import xyz.block.ftl.LanguageTypeMapping +import xyz.block.ftl.TypeAlias +import xyz.block.ftl.TypeAliasMapper + +@TypeAlias( + name = "AnySerializedType", + languageTypeMappings = [LanguageTypeMapping(language = "go", type = "github.com/blockxyz/ftl/test.AnySerializedType")] +) +class AnySerializedTypeMapper : TypeAliasMapper { + override fun encode(`object`: AnySerializedType): JsonNode { + return TextNode.valueOf(`object`.value) + } + + override fun decode(serialized: JsonNode): AnySerializedType { + if (serialized.isTextual) { + return AnySerializedType(serialized.textValue()) + } + throw RuntimeException("Expected a textual value") + } +} diff --git a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/CustomSerializedType.kt b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/CustomSerializedType.kt new file mode 100644 index 0000000000..4fed03cc27 --- /dev/null +++ b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/CustomSerializedType.kt @@ -0,0 +1,3 @@ +package xyz.block.ftl.test + +class CustomSerializedType(val value: String) diff --git a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/CustomSerializedTypeMapper.kt b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/CustomSerializedTypeMapper.kt new file mode 100644 index 0000000000..b7d3d9f5b2 --- /dev/null +++ b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/CustomSerializedTypeMapper.kt @@ -0,0 +1,15 @@ +package xyz.block.ftl.test + +import xyz.block.ftl.TypeAlias +import xyz.block.ftl.TypeAliasMapper + +@TypeAlias(name = "CustomSerializedType") +class CustomSerializedTypeMapper : TypeAliasMapper { + override fun encode(`object`: CustomSerializedType): String { + return `object`.value + } + + override fun decode(serialized: String): CustomSerializedType { + return CustomSerializedType(serialized) + } +} diff --git a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt index 909d8aac6b..4067096e19 100644 --- a/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt +++ b/jvm-runtime/testdata/kotlin/kotlinmodule/src/main/kotlin/xyz/block/ftl/test/TestInvokeGoFromKotlin.kt @@ -186,3 +186,15 @@ fun optionalTestObjectOptionalFieldsVerb( fun externalTypeVerb(did: Did, client: ExternalTypeVerbClient): Did { return client.call(did) } + +@Export +@Verb +fun stringAliasedType(type: CustomSerializedType): CustomSerializedType { + return type +} + +@Export +@Verb +fun anyAliasedType(type: AnySerializedType): AnySerializedType { + return type +}