Skip to content

Commit

Permalink
feat: docs and fixes for Java external types
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas committed Oct 3, 2024
1 parent e534f24 commit e641f54
Show file tree
Hide file tree
Showing 19 changed files with 426 additions and 31 deletions.
152 changes: 146 additions & 6 deletions docs/content/docs/reference/externaltypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->


```go
//ftl:typealias
type FtlType external.OtherType
Expand All @@ -25,37 +37,165 @@ 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:

```
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 -->

```kotlin
@TypeAlias(name = "OtherType")
class OtherTypeTypeMapper : TypeAliasMapper<OtherType, JsonNode> {
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<OtherType, String> {
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 -->
```java
@TypeAlias(name = "OtherType")
public class OtherTypeTypeMapper implements TypeAliasMapper<OtherType, JsonNode> {
@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<OtherType, String> {
@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 -->

```go
//ftl:typealias
//ftl:typemap kotlin "com.external.other.OtherType"
//ftl:typemap java "com.external.other.OtherType"
type FtlType external.OtherType
```

<!-- kotlin -->

```kotlin
@TypeAlias(
name = "OtherType",
languageTypeMappings = [LanguageTypeMapping(language = "go", type = "github.com/external.OtherType")]
)
```

<!-- java -->

```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
Expand Down
6 changes: 3 additions & 3 deletions internal/integration/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down
25 changes: 24 additions & 1 deletion internal/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> 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());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<String, String> 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)));
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@
String name();

String module() default "";

LanguageTypeMapping[] languageTypeMappings() default {};
}
Loading

0 comments on commit e641f54

Please sign in to comment.