diff --git a/.gitignore b/.gitignore
index 5a02738e2b..7836f997a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,9 @@ examples/**/go.work
examples/**/go.work.sum
testdata/**/go.work
testdata/**/go.work.sum
+**/testdata/**/ftl-module-schema/
go-runtime/schema/testdata/test/test.go
+.cache
# Leaving old _ftl for now to avoid old stuff getting checked in
**/testdata/**/_ftl
diff --git a/Justfile b/Justfile
index 1d1441cc7a..8bd9e6aa3e 100644
--- a/Justfile
+++ b/Justfile
@@ -65,8 +65,8 @@ build +tools: build-protos build-zips build-frontend
build-backend:
just build ftl ftl-controller ftl-runner
-build-java:
- mvn -f java-runtime/ftl-runtime install
+build-java *args:
+ mvn -f java-runtime/ftl-runtime install {{args}}
export DATABASE_URL := "postgres://postgres:secret@localhost:15432/ftl?sslmode=disable"
diff --git a/backend/controller/leases/lease_integration_test.go b/backend/controller/leases/lease_integration_test.go
index 3af001258c..64f2d7bba3 100644
--- a/backend/controller/leases/lease_integration_test.go
+++ b/backend/controller/leases/lease_integration_test.go
@@ -19,10 +19,11 @@ import (
func TestLease(t *testing.T) {
in.Run(t,
+ in.WithLanguages("go", "java"),
in.CopyModule("leases"),
in.Build("leases"),
// checks if leases work in a unit test environment
- in.ExecModuleTest("leases"),
+ in.IfLanguage("go", in.ExecModuleTest("leases")),
in.Deploy("leases"),
// checks if it leases work with a real controller
func(t testing.TB, ic in.TestContext) {
@@ -34,6 +35,7 @@ func TestLease(t *testing.T) {
Verb: &schemapb.Ref{Module: "leases", Name: "acquire"},
Body: []byte("{}"),
}))
+ assert.NoError(t, err)
if respErr := resp.Msg.GetError(); respErr != nil {
return fmt.Errorf("received error on first call: %v", respErr)
}
diff --git a/backend/controller/leases/testdata/java/leases/ftl.toml b/backend/controller/leases/testdata/java/leases/ftl.toml
new file mode 100644
index 0000000000..970a945305
--- /dev/null
+++ b/backend/controller/leases/testdata/java/leases/ftl.toml
@@ -0,0 +1,2 @@
+module = "leases"
+language = "java"
diff --git a/backend/controller/leases/testdata/java/leases/pom.xml b/backend/controller/leases/testdata/java/leases/pom.xml
new file mode 100644
index 0000000000..dd938866c6
--- /dev/null
+++ b/backend/controller/leases/testdata/java/leases/pom.xml
@@ -0,0 +1,141 @@
+
+
+ 4.0.0
+ xyz.block.ftl.examples
+ leases
+ 1.0-SNAPSHOT
+
+
+ 1.0-SNAPSHOT
+ 3.13.0
+ 2.0.0
+ 17
+ UTF-8
+ UTF-8
+ quarkus-bom
+ io.quarkus.platform
+ 3.12.3
+ true
+ 3.2.5
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ ${quarkus.platform.artifact-id}
+ ${quarkus.platform.version}
+ pom
+ import
+
+
+
+
+
+
+ xyz.block
+ ftl-java-runtime
+ 1.0-SNAPSHOT
+
+
+ io.quarkus
+ quarkus-kotlin
+
+
+ io.quarkus
+ quarkus-jackson
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+
+
+ io.rest-assured
+ kotlin-extensions
+ test
+
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+ native-image-agent
+
+
+
+
+
+ maven-compiler-plugin
+ ${compiler-plugin.version}
+
+
+ -parameters
+
+
+
+
+ maven-surefire-plugin
+ ${surefire-plugin.version}
+
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+ maven-failsafe-plugin
+ ${surefire-plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+
+
+
+
+ native
+
+
+ native
+
+
+
+ false
+ true
+
+
+
+
diff --git a/backend/controller/leases/testdata/java/leases/src/main/java/xyz/block/ftl/java/test/leases/TestLeases.java b/backend/controller/leases/testdata/java/leases/src/main/java/xyz/block/ftl/java/test/leases/TestLeases.java
new file mode 100644
index 0000000000..e9fd132a7c
--- /dev/null
+++ b/backend/controller/leases/testdata/java/leases/src/main/java/xyz/block/ftl/java/test/leases/TestLeases.java
@@ -0,0 +1,22 @@
+package xyz.block.ftl.java.test.leases;
+
+import io.quarkus.logging.Log;
+import xyz.block.ftl.Export;
+import xyz.block.ftl.LeaseClient;
+import xyz.block.ftl.Verb;
+
+import java.time.Duration;
+
+public class TestLeases {
+
+ @Export
+ @Verb
+ public void acquire(LeaseClient leaseClient) throws Exception {
+ Log.info("Acquiring lease");
+ try (var lease = leaseClient.acquireLease(Duration.ofSeconds(10), "lease")) {
+ Log.info("Acquired lease");
+ Thread.sleep(5000);
+ }
+ }
+
+}
diff --git a/examples/kotlin/echo/pom.xml b/examples/kotlin/echo/pom.xml
index f4a3a46581..9fee587a75 100644
--- a/examples/kotlin/echo/pom.xml
+++ b/examples/kotlin/echo/pom.xml
@@ -3,7 +3,7 @@
4.0.0
xyz.block.ftl.examples
echo
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
1.0-SNAPSHOT
@@ -35,7 +35,7 @@
xyz.block
ftl-java-runtime
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
io.quarkus
diff --git a/examples/kotlin/time/pom.xml b/examples/kotlin/time/pom.xml
index 92190cd367..a249eddc10 100644
--- a/examples/kotlin/time/pom.xml
+++ b/examples/kotlin/time/pom.xml
@@ -3,7 +3,7 @@
4.0.0
xyz.block.ftl.examples
time
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
1.0-SNAPSHOT
@@ -35,7 +35,7 @@
xyz.block
ftl-java-runtime
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
io.quarkus
diff --git a/integration/actions.go b/integration/actions.go
index 7875fd7429..a3b4e76884 100644
--- a/integration/actions.go
+++ b/integration/actions.go
@@ -12,6 +12,7 @@ import (
"net/url"
"os"
"path/filepath"
+ "slices"
"strings"
"testing"
"time"
@@ -95,6 +96,17 @@ func Chain(actions ...Action) Action {
}
}
+// SubTests runs a list of individual actions as separate tests
+func SubTests(tests ...SubTest) Action {
+ return func(t testing.TB, ic TestContext) {
+ for _, test := range tests {
+ ic.Run(test.Name, func(t *testing.T) {
+ ic.AssertWithRetry(t, test.Action)
+ })
+ }
+ }
+}
+
// Repeat an action N times.
func Repeat(n int, action Action) Action {
return func(t testing.TB, ic TestContext) {
@@ -503,6 +515,18 @@ func HttpCall(method string, path string, headers map[string][]string, body []by
}
}
+func IfLanguage(language string, action Action) Action {
+ return IfLanguages(action, language)
+}
+
+func IfLanguages(action Action, languages ...string) Action {
+ return func(t testing.TB, ic TestContext) {
+ if slices.Contains(languages, ic.language) {
+ action(t, ic)
+ }
+ }
+}
+
// Run "go test" in the given module.
func ExecModuleTest(module string) Action {
return Chdir(module, Exec("go", "test", "./..."))
diff --git a/integration/harness.go b/integration/harness.go
index b9a192ecc2..29bcf9332a 100644
--- a/integration/harness.go
+++ b/integration/harness.go
@@ -9,6 +9,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "slices"
"sync"
"syscall"
"testing"
@@ -78,8 +79,10 @@ func WithEnvar(key, value string) Option {
}
}
-// WithJava is a Run* option that ensures the Java runtime is built.
-func WithJava() Option {
+// WithJavaBuild is a Run* option that ensures the Java runtime is built.
+// If the test languages contain java this is not necessary, as it is implied
+// Note that this will not actually add Java as a language under test
+func WithJavaBuild() Option {
return func(o *options) {
o.requireJava = true
}
@@ -176,13 +179,14 @@ func run(t *testing.T, actionsOrOptions ...ActionOrOption) {
Infof("Building ftl")
err = ftlexec.Command(ctx, log.Debug, rootDir, "just", "build", "ftl").RunBuffered(ctx)
assert.NoError(t, err)
- if opts.requireJava {
- err = ftlexec.Command(ctx, log.Debug, rootDir, "just", "build-java").RunBuffered(ctx)
+ if opts.requireJava || slices.Contains(opts.languages, "java") {
+ err = ftlexec.Command(ctx, log.Debug, rootDir, "just", "build-java", "-DskipTests").RunBuffered(ctx)
assert.NoError(t, err)
}
})
for _, language := range opts.languages {
+ ctx, done := context.WithCancel(ctx)
t.Run(language, func(t *testing.T) {
verbs := rpc.Dial(ftlv1connect.NewVerbServiceClient, "http://localhost:8892", log.Debug)
@@ -203,6 +207,8 @@ func run(t *testing.T, actionsOrOptions ...ActionOrOption) {
workDir: tmpDir,
binDir: binDir,
Verbs: verbs,
+ realT: t,
+ language: language,
}
if opts.startController {
@@ -222,6 +228,7 @@ func run(t *testing.T, actionsOrOptions ...ActionOrOption) {
ic.AssertWithRetry(t, action)
}
})
+ done()
}
}
@@ -235,10 +242,18 @@ type TestContext struct {
testData string
// Path to the "bin" directory.
binDir string
+ // The Language under test
+ language string
Controller ftlv1connect.ControllerServiceClient
Console pbconsoleconnect.ConsoleServiceClient
Verbs ftlv1connect.VerbServiceClient
+
+ realT *testing.T
+}
+
+func (i TestContext) Run(name string, f func(t *testing.T)) bool {
+ return i.realT.Run(name, f)
}
// WorkingDir returns the temporary directory the test is executing in.
@@ -283,6 +298,11 @@ func (i TestContext) runAssertionOnce(t testing.TB, assertion Action) (err error
type Action func(t testing.TB, ic TestContext)
+type SubTest struct {
+ Name string
+ Action Action
+}
+
type logWriter struct {
mu sync.Mutex
logger interface{ Log(...any) }
diff --git a/java-runtime/ftl-runtime/deployment/pom.xml b/java-runtime/ftl-runtime/deployment/pom.xml
index db056f496e..e0cf812d51 100644
--- a/java-runtime/ftl-runtime/deployment/pom.xml
+++ b/java-runtime/ftl-runtime/deployment/pom.xml
@@ -5,7 +5,7 @@
xyz.block
ftl-java-runtime-parent
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
ftl-java-runtime-deployment
Ftl Java Runtime - Deployment
diff --git a/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FTLCodeGenerator.java b/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FTLCodeGenerator.java
index dce8144fed..95c7f1b6b4 100644
--- a/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FTLCodeGenerator.java
+++ b/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FTLCodeGenerator.java
@@ -4,7 +4,7 @@
import java.lang.annotation.Retention;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.time.Instant;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -116,20 +116,20 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
typeBuilder.addSuperinterface(ClassName.get(VerbClientEmpty.class));
} else if (verb.getRequest().hasUnit()) {
typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(VerbClientSource.class),
- toJavaTypeName(verb.getResponse(), typeAliasMap)));
+ toJavaTypeName(verb.getResponse(), typeAliasMap, true)));
typeBuilder.addMethod(MethodSpec.methodBuilder("call")
.returns(toAnnotatedJavaTypeName(verb.getResponse(), typeAliasMap))
.addModifiers(Modifier.ABSTRACT).addModifiers(Modifier.PUBLIC).build());
} else if (verb.getResponse().hasUnit()) {
typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(VerbClientSink.class),
- toJavaTypeName(verb.getRequest(), typeAliasMap)));
+ toJavaTypeName(verb.getRequest(), typeAliasMap, true)));
typeBuilder.addMethod(MethodSpec.methodBuilder("call").returns(TypeName.VOID)
.addParameter(toAnnotatedJavaTypeName(verb.getRequest(), typeAliasMap), "value")
.addModifiers(Modifier.ABSTRACT).addModifiers(Modifier.PUBLIC).build());
} else {
typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(VerbClient.class),
- toJavaTypeName(verb.getRequest(), typeAliasMap),
- toJavaTypeName(verb.getResponse(), typeAliasMap)));
+ toJavaTypeName(verb.getRequest(), typeAliasMap, true),
+ toJavaTypeName(verb.getResponse(), typeAliasMap, true)));
typeBuilder.addMethod(MethodSpec.methodBuilder("call")
.returns(toAnnotatedJavaTypeName(verb.getResponse(), typeAliasMap))
.addParameter(toAnnotatedJavaTypeName(verb.getRequest(), typeAliasMap), "value")
@@ -274,29 +274,29 @@ private String toJavaName(String name) {
}
private TypeName toAnnotatedJavaTypeName(Type type, Map typeAliasMap) {
- var results = toJavaTypeName(type, typeAliasMap);
+ var results = toJavaTypeName(type, typeAliasMap, 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) {
+ private TypeName toJavaTypeName(Type type, Map typeAliasMap, boolean boxPrimitives) {
if (type.hasArray()) {
return ParameterizedTypeName.get(ClassName.get(List.class),
- toJavaTypeName(type.getArray().getElement(), typeAliasMap));
+ toJavaTypeName(type.getArray().getElement(), typeAliasMap, false));
} else if (type.hasString()) {
return ClassName.get(String.class);
} else if (type.hasOptional()) {
- return toJavaTypeName(type.getOptional().getType(), typeAliasMap);
+ // Always box for optional, as normal primities can't be null
+ return toJavaTypeName(type.getOptional().getType(), typeAliasMap, true);
} else if (type.hasRef()) {
if (type.getRef().getModule().isEmpty()) {
return TypeVariableName.get(type.getRef().getName());
}
-
Key key = new Key(type.getRef().getModule(), type.getRef().getName());
if (typeAliasMap.containsKey(key)) {
- return toJavaTypeName(typeAliasMap.get(key), typeAliasMap);
+ return toJavaTypeName(typeAliasMap.get(key), typeAliasMap, boxPrimitives);
}
var params = type.getRef().getTypeParametersList();
ClassName className = ClassName.get(PACKAGE_PREFIX + type.getRef().getModule(), type.getRef().getName());
@@ -304,22 +304,23 @@ private TypeName toJavaTypeName(Type type, Map typeAliasMap) {
return className;
}
List javaTypes = params.stream()
- .map(s -> s.hasUnit() ? WildcardTypeName.subtypeOf(Object.class) : toJavaTypeName(s, typeAliasMap))
+ .map(s -> s.hasUnit() ? WildcardTypeName.subtypeOf(Object.class) : toJavaTypeName(s, typeAliasMap, 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),
- toJavaTypeName(type.getMap().getValue(), typeAliasMap));
+ return ParameterizedTypeName.get(ClassName.get(Map.class),
+ toJavaTypeName(type.getMap().getKey(), typeAliasMap, true),
+ toJavaTypeName(type.getMap().getValue(), typeAliasMap, true));
} else if (type.hasTime()) {
- return ClassName.get(Instant.class);
+ return ClassName.get(ZonedDateTime.class);
} else if (type.hasInt()) {
- return TypeName.LONG;
+ return boxPrimitives ? ClassName.get(Long.class) : TypeName.LONG;
} else if (type.hasUnit()) {
return TypeName.VOID;
} else if (type.hasBool()) {
- return TypeName.BOOLEAN;
+ return boxPrimitives ? ClassName.get(Boolean.class) : TypeName.BOOLEAN;
} else if (type.hasFloat()) {
- return TypeName.DOUBLE;
+ return boxPrimitives ? ClassName.get(Double.class) : TypeName.DOUBLE;
} else if (type.hasBytes()) {
return ArrayTypeName.of(TypeName.BYTE);
} else if (type.hasAny()) {
diff --git a/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java b/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java
index 538ecd739d..2703a30bec 100644
--- a/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java
+++ b/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java
@@ -6,7 +6,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
+import java.time.Instant;
import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
@@ -20,10 +22,12 @@
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.ArrayType;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.PrimitiveType;
import org.jboss.jandex.VoidType;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.common.model.MethodParameter;
@@ -85,6 +89,7 @@
import xyz.block.ftl.v1.CallRequest;
import xyz.block.ftl.v1.schema.Array;
import xyz.block.ftl.v1.schema.Bool;
+import xyz.block.ftl.v1.schema.Bytes;
import xyz.block.ftl.v1.schema.Data;
import xyz.block.ftl.v1.schema.Decl;
import xyz.block.ftl.v1.schema.Field;
@@ -123,6 +128,9 @@ class FtlProcessor {
public static final DotName OFFSET_DATE_TIME = DotName.createSimple(OffsetDateTime.class.getName());
public static final DotName GENERATED_REF = DotName.createSimple(GeneratedRef.class);
public static final DotName LEASE_CLIENT = DotName.createSimple(LeaseClient.class);
+ public static final DotName INSTANT = DotName.createSimple(Instant.class);
+ public static final DotName ZONED_DATE_TIME = DotName.createSimple(ZonedDateTime.class);
+ public static final DotName NOT_NULL = DotName.createSimple(NotNull.class);
@BuildStep
ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem) {
@@ -326,9 +334,11 @@ public void registerVerbs(CombinedIndexBuildItem index,
output = outputTargetBuildItem.getOutputDirectory().resolve("main");
try (var out = Files.newOutputStream(output)) {
- out.write("""
- #!/bin/bash
- exec java -jar quarkus-app/quarkus-run.jar""".getBytes(StandardCharsets.UTF_8));
+ out.write(
+ """
+ #!/bin/bash
+ exec java $FTL_JVM_OPTS -jar quarkus-app/quarkus-run.jar"""
+ .getBytes(StandardCharsets.UTF_8));
}
var perms = Files.getPosixFilePermissions(output);
EnumSet newPerms = EnumSet.copyOf(perms);
@@ -536,9 +546,33 @@ private static Class> loadClass(org.jboss.jandex.Type param) throws ClassNotFo
default:
throw new RuntimeException("Unknown primitive type " + param.asPrimitiveType().primitive());
}
- } else {
- throw new RuntimeException("Unknown type " + param.kind());
+ } else if (param.kind() == org.jboss.jandex.Type.Kind.ARRAY) {
+ ArrayType array = param.asArrayType();
+ if (array.componentType().kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) {
+ switch (array.componentType().asPrimitiveType().primitive()) {
+ case BOOLEAN:
+ return boolean[].class;
+ case BYTE:
+ return byte[].class;
+ case SHORT:
+ return short[].class;
+ case INT:
+ return int[].class;
+ case LONG:
+ return long[].class;
+ case FLOAT:
+ return float[].class;
+ case DOUBLE:
+ return double[].class;
+ case CHAR:
+ return char[].class;
+ default:
+ throw new RuntimeException("Unknown primitive type " + param.asPrimitiveType().primitive());
+ }
+ }
}
+ throw new RuntimeException("Unknown type " + param.kind());
+
}
/**
@@ -592,13 +626,27 @@ private Type buildType(ExtractionContext context, org.jboss.jandex.Type type) {
return Type.newBuilder().setUnit(Unit.newBuilder().build()).build();
}
case ARRAY -> {
+ ArrayType arrayType = type.asArrayType();
+ if (arrayType.componentType().kind() == org.jboss.jandex.Type.Kind.PRIMITIVE && arrayType
+ .componentType().asPrimitiveType().primitive() == PrimitiveType.Primitive.BYTE) {
+ return Type.newBuilder().setBytes(Bytes.newBuilder().build()).build();
+ }
return Type.newBuilder()
- .setArray(Array.newBuilder().setElement(buildType(context, type.asArrayType().componentType())).build())
+ .setArray(Array.newBuilder().setElement(buildType(context, arrayType.componentType())).build())
.build();
}
case CLASS -> {
var clazz = type.asClassType();
var info = context.index().getComputingIndex().getClassByName(clazz.name());
+
+ PrimitiveType unboxed = PrimitiveType.unbox(clazz);
+ if (unboxed != null) {
+ Type primitive = buildType(context, unboxed);
+ if (type.hasAnnotation(NOT_NULL)) {
+ return primitive;
+ }
+ return Type.newBuilder().setOptional(Optional.newBuilder().setType(primitive)).build();
+ }
if (info != null && info.hasDeclaredAnnotation(GENERATED_REF)) {
var ref = info.declaredAnnotation(GENERATED_REF);
return Type.newBuilder()
@@ -612,6 +660,12 @@ private Type buildType(ExtractionContext context, org.jboss.jandex.Type type) {
if (clazz.name().equals(OFFSET_DATE_TIME)) {
return Type.newBuilder().setTime(Time.newBuilder().build()).build();
}
+ if (clazz.name().equals(INSTANT)) {
+ return Type.newBuilder().setTime(Time.newBuilder().build()).build();
+ }
+ if (clazz.name().equals(ZONED_DATE_TIME)) {
+ return Type.newBuilder().setTime(Time.newBuilder().build()).build();
+ }
var existing = context.dataElements.get(new TypeKey(clazz.name().toString(), List.of()));
if (existing != null) {
return Type.newBuilder().setRef(existing).build();
@@ -636,6 +690,7 @@ private Type buildType(ExtractionContext context, org.jboss.jandex.Type type) {
.setValue(buildType(context, paramType.arguments().get(0))))
.build();
} else if (paramType.name().equals(DotNames.OPTIONAL)) {
+ //TODO: optional kinda sucks
return Type.newBuilder()
.setOptional(Optional.newBuilder().setType(buildType(context, paramType.arguments().get(0))))
.build();
diff --git a/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java b/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java
index 367393890a..5459411459 100644
--- a/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java
+++ b/java-runtime/ftl-runtime/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java
@@ -51,7 +51,7 @@ TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer> signatures = new LinkedHashSet<>();
+ signatures.add(Map.entry(returnType.name().toString(), paramType.name().toString()));
+ signatures.add(Map.entry(Object.class.getName(), Object.class.getName()));
+ for (var method : iface.methods()) {
+ if (method.name().equals("call") && method.parameters().size() == 1) {
+ signatures.add(Map.entry(method.returnType().name().toString(),
+ method.parameters().get(0).type().name().toString()));
+ }
+ }
+ for (var sig : signatures) {
+
+ var publish = cc.getMethodCreator("call", sig.getKey(),
+ sig.getValue());
+ var helper = publish.invokeStaticMethod(
+ MethodDescriptor.ofMethod(VerbClientHelper.class, "instance", VerbClientHelper.class));
+ var results = publish.invokeVirtualMethod(
+ MethodDescriptor.ofMethod(VerbClientHelper.class, "call", Object.class, String.class,
+ String.class, Object.class, Class.class, boolean.class, boolean.class),
+ helper, publish.load(name), publish.load(module), publish.getMethodParam(0),
+ publish.loadClass(returnType.name().toString()), publish.load(false),
+ publish.load(false));
+ publish.returnValue(results);
+ }
+
clients.put(iface.name(),
new VerbClientBuildItem.DiscoveredClients(name, module, cc.getClassName()));
}
@@ -113,24 +119,25 @@ VerbClientBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer signatures = new LinkedHashSet<>();
+ signatures.add(paramType.name().toString());
+ signatures.add(Object.class.getName());
+ for (var method : iface.methods()) {
+ if (method.name().equals("call") && method.parameters().size() == 1) {
+ signatures.add(method.parameters().get(0).type().name().toString());
+ }
+ }
+ for (var sig : signatures) {
+ var publish = cc.getMethodCreator("call", void.class, sig);
+ var helper = publish.invokeStaticMethod(
+ MethodDescriptor.ofMethod(VerbClientHelper.class, "instance", VerbClientHelper.class));
+ publish.invokeVirtualMethod(
+ MethodDescriptor.ofMethod(VerbClientHelper.class, "call", Object.class, String.class,
+ String.class, Object.class, Class.class, boolean.class, boolean.class),
+ helper, publish.load(name), publish.load(module), publish.getMethodParam(0),
+ publish.loadClass(Void.class), publish.load(false), publish.load(false));
+ publish.returnVoid();
+ }
clients.put(iface.name(),
new VerbClientBuildItem.DiscoveredClients(name, module, cc.getClassName()));
}
@@ -150,25 +157,27 @@ VerbClientBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer signatures = new LinkedHashSet<>();
+ signatures.add(returnType.name().toString());
+ signatures.add(Object.class.getName());
+ for (var method : iface.methods()) {
+ if (method.name().equals("call") && method.parameters().size() == 0) {
+ signatures.add(method.returnType().name().toString());
+ }
+ }
+ for (var sig : signatures) {
+ var publish = cc.getMethodCreator("call", sig);
+ var helper = publish.invokeStaticMethod(
+ MethodDescriptor.ofMethod(VerbClientHelper.class, "instance", VerbClientHelper.class));
+ var results = publish.invokeVirtualMethod(
+ MethodDescriptor.ofMethod(VerbClientHelper.class, "call", Object.class, String.class,
+ String.class, Object.class, Class.class, boolean.class, boolean.class),
+ helper, publish.load(name), publish.load(module), publish.loadNull(),
+ publish.loadClass(returnType.name().toString()), publish.load(false),
+ publish.load(false));
+ publish.returnValue(results);
+ }
- publish = cc.getMethodCreator("call", Object.class);
- helper = publish.invokeStaticMethod(
- MethodDescriptor.ofMethod(VerbClientHelper.class, "instance", VerbClientHelper.class));
- results = publish.invokeVirtualMethod(
- MethodDescriptor.ofMethod(VerbClientHelper.class, "call", Object.class, String.class,
- String.class, Object.class, Class.class, boolean.class, boolean.class),
- helper, publish.load(name), publish.load(module), publish.loadNull(),
- publish.loadClass(returnType.name().toString()), publish.load(false), publish.load(false));
- publish.returnValue(results);
clients.put(iface.name(),
new VerbClientBuildItem.DiscoveredClients(name, module, cc.getClassName()));
}
diff --git a/java-runtime/ftl-runtime/integration-tests/pom.xml b/java-runtime/ftl-runtime/integration-tests/pom.xml
index 11c116b7c7..823f3a9336 100644
--- a/java-runtime/ftl-runtime/integration-tests/pom.xml
+++ b/java-runtime/ftl-runtime/integration-tests/pom.xml
@@ -5,7 +5,7 @@
xyz.block
ftl-java-runtime-parent
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
ftl-java-runtime-integration-tests
Ftl Java Runtime - Integration Tests
diff --git a/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java b/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java
index cf91946a90..55f9fe3007 100644
--- a/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java
+++ b/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java
@@ -43,4 +43,9 @@ public String hello(String name, EchoClient echoClient) {
public void publish(Person person, MyTopic topic) {
topic.publish(person);
}
+
+ @Verb
+ public byte[] bytes(byte[] bytes) {
+ return bytes;
+ }
}
diff --git a/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/MyTopic.java b/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/MyTopic.java
index f5b381f0da..0e5ef1e996 100644
--- a/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/MyTopic.java
+++ b/java-runtime/ftl-runtime/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/MyTopic.java
@@ -5,6 +5,6 @@
import xyz.block.ftl.TopicDefinition;
@Export
-@TopicDefinition(name = "testTopic")
+@TopicDefinition(value = "testTopic")
public interface MyTopic extends Topic {
}
diff --git a/java-runtime/ftl-runtime/integration-tests/src/test/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResourceTest.java b/java-runtime/ftl-runtime/integration-tests/src/test/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResourceTest.java
index ef47c2b6a1..77b17a2df0 100644
--- a/java-runtime/ftl-runtime/integration-tests/src/test/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResourceTest.java
+++ b/java-runtime/ftl-runtime/integration-tests/src/test/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResourceTest.java
@@ -33,6 +33,10 @@ public class FtlJavaRuntimeResourceTest {
@Inject
HelloClient helloClient;
+ @FTLManaged
+ @Inject
+ BytesClient bytesClient;
+
@Test
public void testHelloEndpoint() {
TestVerbServer.registerFakeVerb("echo", "echo", new Function() {
@@ -52,6 +56,11 @@ public void testTopic() {
myVerbClient.call(new Person("Stuart", "Douglas"));
}
+ @Test
+ public void testBytesSerialization() {
+ Assertions.assertArrayEquals(new byte[] { 1, 2 }, bytesClient.call(new byte[] { 1, 2 }));
+ }
+
@VerbClientDefinition(name = "publish")
interface PublishVerbClient extends VerbClientSink {
}
@@ -59,4 +68,8 @@ interface PublishVerbClient extends VerbClientSink {
@VerbClientDefinition(name = "hello")
interface HelloClient extends VerbClient {
}
+
+ @VerbClientDefinition(name = "bytes")
+ interface BytesClient extends VerbClient {
+ }
}
diff --git a/java-runtime/ftl-runtime/pom.xml b/java-runtime/ftl-runtime/pom.xml
index 4ff40b3f4f..b4b63c22cc 100644
--- a/java-runtime/ftl-runtime/pom.xml
+++ b/java-runtime/ftl-runtime/pom.xml
@@ -4,7 +4,7 @@
4.0.0
xyz.block
ftl-java-runtime-parent
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
pom
Ftl Java Runtime - Parent
diff --git a/java-runtime/ftl-runtime/runtime/pom.xml b/java-runtime/ftl-runtime/runtime/pom.xml
index 9ea5ec0c64..63b71e7c15 100644
--- a/java-runtime/ftl-runtime/runtime/pom.xml
+++ b/java-runtime/ftl-runtime/runtime/pom.xml
@@ -6,7 +6,7 @@
xyz.block
ftl-java-runtime-parent
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
ftl-java-runtime
Ftl Java Runtime - Runtime
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/LeaseClient.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/LeaseClient.java
index 4f5cc51242..759e7785f0 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/LeaseClient.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/LeaseClient.java
@@ -7,5 +7,13 @@
*/
public interface LeaseClient {
- void acquireLease(Duration duration, String... keys) throws LeaseFailedException;
+ /**
+ * Acquire a lease for the given keys. The lease will be held for the given duration.
+ *
+ * @param duration The time to acquire the lease for
+ * @param keys The lease keys
+ * @return A handle that can be used to release the lease
+ * @throws LeaseFailedException
+ */
+ LeaseHandle acquireLease(Duration duration, String... keys) throws LeaseFailedException;
}
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/LeaseHandle.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/LeaseHandle.java
new file mode 100644
index 0000000000..6d1eff7a7f
--- /dev/null
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/LeaseHandle.java
@@ -0,0 +1,7 @@
+package xyz.block.ftl;
+
+public interface LeaseHandle extends AutoCloseable {
+
+ public void close();
+
+}
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/Topic.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/Topic.java
index 16170fe24e..2b1d2c66ed 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/Topic.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/Topic.java
@@ -8,5 +8,5 @@
*/
public interface Topic {
- void publish(T object);
+ void publish(T object);
}
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/TopicDefinition.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/TopicDefinition.java
index a6482006d6..7647133cc0 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/TopicDefinition.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/TopicDefinition.java
@@ -12,6 +12,6 @@
*
* @return The name of the topic
*/
- String name();
+ String value();
}
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java
index 25eb62fb65..dbf1d3d7ff 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java
@@ -3,11 +3,10 @@
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
-import java.util.Deque;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.LinkedBlockingDeque;
+import jakarta.annotation.PreDestroy;
import jakarta.inject.Singleton;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -20,6 +19,7 @@
import io.quarkus.runtime.Startup;
import xyz.block.ftl.LeaseClient;
import xyz.block.ftl.LeaseFailedException;
+import xyz.block.ftl.LeaseHandle;
import xyz.block.ftl.v1.AcquireLeaseRequest;
import xyz.block.ftl.v1.AcquireLeaseResponse;
import xyz.block.ftl.v1.CallRequest;
@@ -36,12 +36,11 @@
public class FTLController implements LeaseClient {
private static final Logger log = Logger.getLogger(FTLController.class);
final String moduleName;
- private StreamObserver leaseClient;
- private final Deque> leaseWaiters = new LinkedBlockingDeque<>();
private Throwable currentError;
private volatile ModuleContextResponse moduleContextResponse;
private boolean waiters = false;
+ private volatile boolean closed = false;
final VerbServiceGrpc.VerbServiceStub verbService;
final StreamObserver moduleObserver = new StreamObserver<>() {
@@ -68,14 +67,22 @@ public void onError(Throwable throwable) {
waiters = false;
}
}
+ if (!closed) {
+ verbService.getModuleContext(ModuleContextRequest.newBuilder().setModule(moduleName).build(), moduleObserver);
+ }
}
@Override
public void onCompleted() {
- verbService.getModuleContext(ModuleContextRequest.newBuilder().setModule(moduleName).build(), moduleObserver);
+ onError(new RuntimeException("connection closed"));
}
};
+ @PreDestroy
+ void shutdown() {
+
+ }
+
public FTLController(@ConfigProperty(name = "ftl.endpoint", defaultValue = "http://localhost:8892") URI uri,
@ConfigProperty(name = "ftl.module.name") String moduleName) {
this.moduleName = moduleName;
@@ -86,29 +93,6 @@ public FTLController(@ConfigProperty(name = "ftl.endpoint", defaultValue = "http
var channel = channelBuilder.build();
verbService = VerbServiceGrpc.newStub(channel);
verbService.getModuleContext(ModuleContextRequest.newBuilder().setModule(moduleName).build(), moduleObserver);
- synchronized (this) {
- this.leaseClient = verbService.acquireLease(new StreamObserver() {
- @Override
- public void onNext(AcquireLeaseResponse value) {
- leaseWaiters.pop().complete(null);
- }
-
- @Override
- public void onError(Throwable t) {
- leaseWaiters.pop().completeExceptionally(t);
- }
-
- @Override
- public void onCompleted() {
- synchronized (FTLController.this) {
- while (!leaseWaiters.isEmpty()) {
- leaseWaiters.pop().completeExceptionally(new RuntimeException("connection closed"));
- }
- leaseClient = verbService.acquireLease(this);
- }
- }
- });
- }
}
public byte[] getSecret(String secretName) {
@@ -187,21 +171,42 @@ public void onCompleted() {
}
}
- public void acquireLease(Duration duration, String... keys) throws LeaseFailedException {
+ public LeaseHandle acquireLease(Duration duration, String... keys) throws LeaseFailedException {
CompletableFuture> cf = new CompletableFuture<>();
- synchronized (this) {
- leaseWaiters.push(cf);
- leaseClient.onNext(AcquireLeaseRequest.newBuilder().setModule(moduleName)
- .addAllKey(Arrays.asList(keys))
- .setTtl(com.google.protobuf.Duration.newBuilder()
- .setSeconds(duration.toSeconds()))
- .build());
- }
+ var client = verbService.acquireLease(new StreamObserver() {
+ @Override
+ public void onNext(AcquireLeaseResponse value) {
+ cf.complete(null);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ cf.completeExceptionally(t);
+ }
+
+ @Override
+ public void onCompleted() {
+ if (!cf.isDone()) {
+ onError(new RuntimeException("stream closed"));
+ }
+ }
+ });
+ client.onNext(AcquireLeaseRequest.newBuilder().setModule(moduleName)
+ .addAllKey(Arrays.asList(keys))
+ .setTtl(com.google.protobuf.Duration.newBuilder()
+ .setSeconds(duration.toSeconds()))
+ .build());
try {
cf.get();
} catch (Exception e) {
- throw new LeaseFailedException(e);
+ throw new LeaseFailedException("lease already held", e);
}
+ return new LeaseHandle() {
+ @Override
+ public void close() {
+ client.onCompleted();
+ }
+ };
}
private ModuleContextResponse getModuleContext() {
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java
index 5ee6413076..9b4b03f7b1 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java
@@ -26,6 +26,7 @@ public void registerVerb(String module, String verbName, String methodName, List
//TODO: this sucks
try {
var method = verbHandlerClass.getDeclaredMethod(methodName, parameterTypes.toArray(new Class[0]));
+ method.setAccessible(true);
var handlerInstance = Arc.container().instance(verbHandlerClass);
Arc.container().instance(VerbRegistry.class).get().register(module, verbName, handlerInstance, method,
paramMappers, allowNullReturn);
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java
index b17d9f94fd..5f19e2ca2d 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/JsonSerializationConfig.java
@@ -1,9 +1,23 @@
package xyz.block.ftl.runtime;
+import java.io.IOException;
+import java.util.Base64;
+
import jakarta.enterprise.event.Observes;
+import jakarta.json.stream.JsonGenerator;
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import io.quarkus.runtime.StartupEvent;
@@ -14,5 +28,41 @@ public class JsonSerializationConfig {
void startup(@Observes StartupEvent event, ObjectMapper mapper) {
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+
+ SimpleModule module = new SimpleModule("ByteArraySerializer", new Version(1, 0, 0, ""));
+ module.addSerializer(byte[].class, new ByteArraySerializer());
+ module.addDeserializer(byte[].class, new ByteArrayDeserializer());
+ mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+ mapper.registerModule(module);
+ }
+
+ public static class ByteArraySerializer extends StdSerializer {
+
+ public ByteArraySerializer() {
+ super(byte[].class);
+ }
+
+ @Override
+ public void serialize(byte[] value, com.fasterxml.jackson.core.JsonGenerator gen, SerializerProvider provider)
+ throws IOException {
+ gen.writeString(Base64.getEncoder().encodeToString(value));
+
+ }
}
+
+ public static class ByteArrayDeserializer extends StdDeserializer {
+
+ public ByteArrayDeserializer() {
+ super(byte[].class);
+ }
+
+ @Override
+ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
+ JsonNode node = p.getCodec().readTree(p);
+ String base64 = node.asText();
+ return Base64.getDecoder().decode(base64);
+ }
+
+ }
+
}
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java
index 714f197a05..8d5103c2ad 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbHandler.java
@@ -18,9 +18,13 @@ public VerbHandler(VerbRegistry registry) {
@Override
public void call(CallRequest request, StreamObserver responseObserver) {
- var response = registry.invoke(request);
- responseObserver.onNext(response);
- responseObserver.onCompleted();
+ try {
+ var response = registry.invoke(request);
+ responseObserver.onNext(response);
+ responseObserver.onCompleted();
+ } catch (Exception e) {
+ responseObserver.onError(e);
+ }
}
@Override
diff --git a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java
index 42166c0719..2208fe51ea 100644
--- a/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java
+++ b/java-runtime/ftl-runtime/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java
@@ -1,6 +1,7 @@
package xyz.block.ftl.runtime;
import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -91,9 +92,15 @@ public CallResponse handle(CallRequest in) {
var mappedResponse = mapper.writer().writeValueAsBytes(ret);
return CallResponse.newBuilder().setBody(ByteString.copyFrom(mappedResponse)).build();
}
- } catch (Exception e) {
- log.errorf(e, "Failed to invoke verb %s.%s", in.getVerb().getModule(), in.getVerb().getName());
- return CallResponse.newBuilder().setError(CallResponse.Error.newBuilder().setMessage(e.getMessage()).build())
+ } catch (Throwable e) {
+ if (e.getClass() == InvocationTargetException.class) {
+ e = e.getCause();
+ }
+ var message = String.format("Failed to invoke verb %s.%s", in.getVerb().getModule(), in.getVerb().getName());
+ log.error(message, e);
+ return CallResponse.newBuilder()
+ .setError(CallResponse.Error.newBuilder().setStack(e.toString())
+ .setMessage(message + " " + e.getMessage()).build())
.build();
}
}
diff --git a/java-runtime/ftl-runtime/test-framework/pom.xml b/java-runtime/ftl-runtime/test-framework/pom.xml
index c66028219f..df940f2032 100644
--- a/java-runtime/ftl-runtime/test-framework/pom.xml
+++ b/java-runtime/ftl-runtime/test-framework/pom.xml
@@ -5,7 +5,7 @@
xyz.block
ftl-java-runtime-parent
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
ftl-java-test-framework
Ftl Java Runtime - Test Framework
diff --git a/java-runtime/java_integration_test.go b/java-runtime/java_integration_test.go
index 0680b4989f..f2f3cb899c 100644
--- a/java-runtime/java_integration_test.go
+++ b/java-runtime/java_integration_test.go
@@ -8,52 +8,159 @@ import (
"github.com/alecthomas/assert/v2"
+ "github.com/TBD54566975/ftl/go-runtime/ftl"
in "github.com/TBD54566975/ftl/integration"
"github.com/alecthomas/repr"
)
func TestJavaToGoCall(t *testing.T) {
- in.Run(t,
- in.WithJava(),
- in.CopyModule("gomodule"),
- in.CopyDir("javamodule", "javamodule"),
- in.Deploy("gomodule"),
- in.Deploy("javamodule"),
- in.Call("javamodule", "timeVerb", in.Obj{}, func(t testing.TB, response in.Obj) {
- message, ok := response["time"].(string)
- assert.True(t, ok, "time is not a string: %s", repr.String(response))
- result, err := time.Parse(time.RFC3339, message)
- assert.NoError(t, err, "time is not a valid RFC3339 time: %s", message)
- assert.True(t, result.After(time.Now().Add(-time.Minute)), "time is not recent: %s", message)
- }),
- // We call both the go and pass through Java versions
- // To make sure the response is the same
- in.Call("gomodule", "emptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) {
- assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response))
- }),
- in.Call("javamodule", "emptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) {
- assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response))
- }),
- in.Call("gomodule", "sinkVerb", "ignored", func(t testing.TB, response in.Obj) {
+
+ exampleObject := TestObject{
+ IntField: 43,
+ FloatField: .2,
+ StringField: "obj",
+ BytesField: []byte{87, 2, 9},
+ BoolField: true,
+ TimeField: time.Now().UTC(),
+ ArrayField: []string{"foo", "bar"},
+ MapField: map[string]string{"gar": "har"},
+ }
+ exampleOptionalFieldsObject := TestObjectOptionalFields{
+ IntField: ftl.Some[int](43),
+ FloatField: ftl.Some[float64](.2),
+ StringField: ftl.Some[string]("obj"),
+ BytesField: ftl.Some[[]byte]([]byte{87, 2, 9}),
+ BoolField: ftl.Some[bool](true),
+ TimeField: ftl.Some[time.Time](time.Now().UTC()),
+ ArrayField: ftl.Some[[]string]([]string{"foo", "bar"}),
+ MapField: ftl.Some[map[string]string](map[string]string{"gar": "har"}),
+ }
+ 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) {
assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response))
- }),
- in.Call("javamodule", "sinkVerb", "ignored", func(t testing.TB, response in.Obj) {
+ })
+ })...)
+ tests = append(tests, PairedTest("sinkVerb", func(module string) in.Action {
+ return in.Call(module, "sinkVerb", "ignored", func(t testing.TB, response in.Obj) {
assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response))
- }),
- in.Call("gomodule", "sourceVerb", in.Obj{}, func(t testing.TB, response string) {
+ })
+ })...)
+ tests = append(tests, PairedTest("sourceVerb", func(module string) in.Action {
+ return in.Call(module, "sourceVerb", in.Obj{}, func(t testing.TB, response string) {
assert.Equal(t, "Source Verb", response, "expecting empty response, got %s", response)
- }),
- in.Call("javamodule", "sourceVerb", in.Obj{}, func(t testing.TB, response string) {
- assert.Equal(t, "Source Verb", response, "expecting empty response, got %s", response)
- }),
- in.Fail(
- in.Call("gomodule", "errorEmptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) {
- assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response))
- }), "verb failed"),
- in.Fail(
- in.Call("gomodule", "errorEmptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) {
+ })
+ })...)
+ tests = append(tests, PairedTest("errorEmptyVerb", func(module string) in.Action {
+ return in.Fail(
+ in.Call(module, "errorEmptyVerb", in.Obj{}, func(t testing.TB, response in.Obj) {
assert.Equal(t, map[string]any{}, response, "expecting empty response, got %s", repr.String(response))
- }), "verb failed"),
+ }), "verb failed")
+ })...)
+ tests = append(tests, PairedVerbTest("intVerb", 124)...)
+ tests = append(tests, PairedVerbTest("floatVerb", 0.123)...)
+ tests = append(tests, PairedVerbTest("stringVerb", "Hello World")...)
+ tests = append(tests, PairedVerbTest("bytesVerb", []byte{1, 2, 3, 0, 1})...)
+ tests = append(tests, PairedVerbTest("boolVerb", true)...)
+ tests = append(tests, PairedVerbTest("stringArrayVerb", []string{"Hello World"})...)
+ tests = append(tests, PairedVerbTest("stringMapVerb", map[string]string{"Hello": "World"})...)
+ tests = append(tests, PairedTest("timeVerb", func(module string) in.Action {
+ now := time.Now().UTC()
+ return in.Call(module, "timeVerb", now.Format(time.RFC3339Nano), func(t testing.TB, response string) {
+ result, err := time.Parse(time.RFC3339Nano, response)
+ assert.NoError(t, err, "time is not a valid RFC3339 time: %s", response)
+ assert.Equal(t, now, result, "times not equal %s %s", now, result)
+ })
+ })...)
+ tests = append(tests, PairedVerbTest("testObjectVerb", exampleObject)...)
+ tests = append(tests, PairedVerbTest("testObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...)
+ tests = append(tests, PairedVerbTest("optionalIntVerb", -3)...)
+ tests = append(tests, PairedVerbTest("optionalFloatVerb", -7.6)...)
+ tests = append(tests, PairedVerbTest("optionalStringVerb", "foo")...)
+ tests = append(tests, PairedVerbTest("optionalBytesVerb", []byte{134, 255, 0})...)
+ tests = append(tests, PairedVerbTest("optionalBoolVerb", false)...)
+ tests = append(tests, PairedVerbTest("optionalStringArrayVerb", []string{"foo"})...)
+ tests = append(tests, PairedVerbTest("optionalStringMapVerb", map[string]string{"Hello": "World"})...)
+ tests = append(tests, PairedTest("optionalTimeVerb", func(module string) in.Action {
+ now := time.Now().UTC()
+ return in.Call(module, "optionalTimeVerb", now.Format(time.RFC3339Nano), func(t testing.TB, response string) {
+ result, err := time.Parse(time.RFC3339Nano, response)
+ assert.NoError(t, err, "time is not a valid RFC3339 time: %s", response)
+ assert.Equal(t, now, result, "times not equal %s %s", now, result)
+ })
+ })...)
+
+ tests = append(tests, PairedVerbTest("optionalTestObjectVerb", exampleObject)...)
+ tests = append(tests, PairedVerbTest("optionalTestObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...)
+ //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]())...)
+ //tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalBytesVerb", ftl.None[[]byte]())...)
+ //tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalBoolVerb", ftl.None[bool]())...)
+ //tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalStringArrayVerb", ftl.None[[]string]())...)
+ //tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalStringMapVerb", ftl.None[map[string]string]())...)
+ //tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalTimeVerb", ftl.None[time.Time]())...)
+ //tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalTestObjectVerb", ftl.None[any]())...)
+ //tests = append(tests, PairedPrefixVerbTest("nilvalue", "optionalTestObjectOptionalFieldsVerb", ftl.None[any]())...)
+
+ in.Run(t,
+ in.WithLanguages("java"),
+ in.CopyModule("gomodule"),
+ in.CopyModule("javamodule"),
+ in.Deploy("gomodule"),
+ in.Deploy("javamodule"),
+ in.SubTests(tests...),
)
}
+
+func PairedTest(name string, testFunc func(module string) in.Action) []in.SubTest {
+ return []in.SubTest{
+ {
+ Name: name + "-go",
+ Action: testFunc("gomodule"),
+ },
+ {
+ Name: name + "-Java",
+ Action: testFunc("javamodule"),
+ },
+ }
+}
+
+func VerbTest[T any](verb string, value T) func(module string) in.Action {
+ return func(module string) in.Action {
+ return in.Call(module, verb, value, func(t testing.TB, response T) {
+ assert.Equal(t, value, response, "verb call results not equal %s %s", value, response)
+ })
+ }
+}
+
+func PairedVerbTest[T any](verb string, value T) []in.SubTest {
+ return PairedTest(verb, VerbTest[T](verb, value))
+}
+
+func PairedPrefixVerbTest[T any](prefex string, verb string, value T) []in.SubTest {
+ return PairedTest(prefex+"-"+verb, VerbTest[T](verb, value))
+}
+
+type TestObject struct {
+ IntField int `json:"intField"`
+ FloatField float64 `json:"floatField"`
+ StringField string `json:"stringField"`
+ BytesField []byte `json:"bytesField"`
+ BoolField bool `json:"boolField"`
+ TimeField time.Time `json:"timeField"`
+ ArrayField []string `json:"arrayField"`
+ MapField map[string]string `json:"mapField"`
+}
+
+type TestObjectOptionalFields struct {
+ IntField ftl.Option[int] `json:"intField"`
+ FloatField ftl.Option[float64] `json:"floatField"`
+ StringField ftl.Option[string] `json:"stringField"`
+ BytesField ftl.Option[[]byte] `json:"bytesField"`
+ BoolField ftl.Option[bool] `json:"boolField"`
+ TimeField ftl.Option[time.Time] `json:"timeField"`
+ ArrayField ftl.Option[[]string] `json:"arrayField"`
+ MapField ftl.Option[map[string]string] `json:"mapField"`
+}
diff --git a/java-runtime/testdata/go/gomodule/go.mod b/java-runtime/testdata/go/gomodule/go.mod
deleted file mode 100644
index 3773c01c5d..0000000000
--- a/java-runtime/testdata/go/gomodule/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module ftl/gomodule
-
-go 1.22.2
-
-replace github.com/TBD54566975/ftl => ./../../../..
diff --git a/java-runtime/testdata/go/gomodule/go.sum b/java-runtime/testdata/go/gomodule/go.sum
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/java-runtime/testdata/go/gomodule/server.go b/java-runtime/testdata/go/gomodule/server.go
deleted file mode 100644
index 65d6d4b0df..0000000000
--- a/java-runtime/testdata/go/gomodule/server.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package gomodule
-
-import (
- "context"
- "fmt"
- "time"
-)
-
-type TimeRequest struct {
-}
-type TimeResponse struct {
- Time time.Time
-}
-
-//ftl:verb export
-func SourceVerb(ctx context.Context) (string, error) {
- return "Source Verb", nil
-}
-
-//ftl:verb export
-func SinkVerb(ctx context.Context, req string) error {
- return nil
-}
-
-//ftl:verb export
-func EmptyVerb(ctx context.Context) error {
- return nil
-}
-
-//ftl:verb export
-func ErrorEmptyVerb(ctx context.Context) error {
- return fmt.Errorf("verb failed")
-}
-
-//ftl:verb export
-func Time(ctx context.Context, req TimeRequest) (TimeResponse, error) {
- return TimeResponse{Time: time.Now()}, nil
-}
diff --git a/java-runtime/testdata/go/javamodule/src/main/java/xyz/block/ftl/java/test/TestInvokeGo.java b/java-runtime/testdata/go/javamodule/src/main/java/xyz/block/ftl/java/test/TestInvokeGo.java
deleted file mode 100644
index da50c7035f..0000000000
--- a/java-runtime/testdata/go/javamodule/src/main/java/xyz/block/ftl/java/test/TestInvokeGo.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package xyz.block.ftl.java.test;
-
-import ftl.gomodule.EmptyVerbClient;
-import ftl.gomodule.ErrorEmptyVerbClient;
-import ftl.gomodule.SinkVerbClient;
-import ftl.gomodule.SourceVerbClient;
-import ftl.gomodule.TimeClient;
-import ftl.gomodule.TimeRequest;
-import ftl.gomodule.TimeResponse;
-import org.jetbrains.annotations.NotNull;
-import xyz.block.ftl.Export;
-import xyz.block.ftl.Verb;
-
-public class TestInvokeGo {
-
- @Export
- @Verb
- public void emptyVerb(EmptyVerbClient emptyVerbClient) {
- emptyVerbClient.call();
- }
-
- @Export
- @Verb
- public void sinkVerb(String input, SinkVerbClient sinkVerbClient) {
- sinkVerbClient.call(input);
- }
-
- @Export
- @Verb
- public String sourceVerb(SourceVerbClient sourceVerbClient) {
- return sourceVerbClient.call();
- }
- @Export
- @Verb
- public void errorEmptyVerb(ErrorEmptyVerbClient client) {
- client.call();
- }
-
- @Export
- @Verb
- public @NotNull TimeResponse timeVerb(TimeClient client) {
- return client.call(new TimeRequest());
- }
-
-}
diff --git a/java-runtime/testdata/go/gomodule/ftl.toml b/java-runtime/testdata/java/gomodule/ftl.toml
similarity index 100%
rename from java-runtime/testdata/go/gomodule/ftl.toml
rename to java-runtime/testdata/java/gomodule/ftl.toml
diff --git a/java-runtime/testdata/java/gomodule/go.mod b/java-runtime/testdata/java/gomodule/go.mod
new file mode 100644
index 0000000000..7c88a20aad
--- /dev/null
+++ b/java-runtime/testdata/java/gomodule/go.mod
@@ -0,0 +1,48 @@
+module ftl/gomodule
+
+go 1.22.2
+
+replace github.com/TBD54566975/ftl => ./../../../..
+
+require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000
+
+require (
+ connectrpc.com/connect v1.16.2 // indirect
+ connectrpc.com/grpcreflect v1.2.0 // indirect
+ connectrpc.com/otelconnect v0.7.1 // indirect
+ github.com/XSAM/otelsql v0.32.0 // indirect
+ github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
+ github.com/alecthomas/concurrency v0.0.2 // indirect
+ github.com/alecthomas/participle/v2 v2.1.1 // indirect
+ github.com/alecthomas/types v0.16.0 // indirect
+ github.com/alessio/shellescape v1.4.2 // indirect
+ github.com/benbjohnson/clock v1.3.5 // indirect
+ github.com/danieljoos/wincred v1.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
+ github.com/hashicorp/cronexpr v1.1.2 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.6.0 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/jpillora/backoff v1.0.0 // indirect
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/multiformats/go-base36 v0.2.0 // indirect
+ github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
+ github.com/swaggest/jsonschema-go v0.3.72 // indirect
+ github.com/swaggest/refl v1.3.0 // indirect
+ github.com/zalando/go-keyring v0.2.5 // indirect
+ go.opentelemetry.io/otel v1.28.0 // indirect
+ go.opentelemetry.io/otel/metric v1.28.0 // indirect
+ go.opentelemetry.io/otel/trace v1.28.0 // indirect
+ golang.org/x/crypto v0.26.0 // indirect
+ golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
+ golang.org/x/mod v0.20.0 // indirect
+ golang.org/x/net v0.28.0 // indirect
+ golang.org/x/sync v0.8.0 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ golang.org/x/text v0.17.0 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+)
diff --git a/java-runtime/testdata/java/gomodule/go.sum b/java-runtime/testdata/java/gomodule/go.sum
new file mode 100644
index 0000000000..9569a27c55
--- /dev/null
+++ b/java-runtime/testdata/java/gomodule/go.sum
@@ -0,0 +1,152 @@
+connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE=
+connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc=
+connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U=
+connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY=
+connectrpc.com/otelconnect v0.7.1 h1:scO5pOb0i4yUE66CnNrHeK1x51yq0bE0ehPg6WvzXJY=
+connectrpc.com/otelconnect v0.7.1/go.mod h1:dh3bFgHBTb2bkqGCeVVOtHJreSns7uu9wwL2Tbz17ms=
+github.com/TBD54566975/scaffolder v1.0.0 h1:QUFSy2wVzumLDg7IHcKC6AP+IYyqWe9Wxiu72nZn5qU=
+github.com/TBD54566975/scaffolder v1.0.0/go.mod h1:auVpczIbOAdIhYDVSruIw41DanxOKB9bSvjf6MEl7Fs=
+github.com/XSAM/otelsql v0.32.0 h1:vDRE4nole0iOOlTaC/Bn6ti7VowzgxK39n3Ll1Kt7i0=
+github.com/XSAM/otelsql v0.32.0/go.mod h1:Ary0hlyVBbaSwo8atZB8Aoothg9s/LBJj/N/p5qDmLM=
+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=
+github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
+github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo=
+github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w=
+github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8=
+github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/types v0.16.0 h1:o9+JSwCRB6DDaWDeR/Mg7v/zh3R+MlknM6DrnDyY7U0=
+github.com/alecthomas/types v0.16.0/go.mod h1:Tswm0qQpjpVq8rn70OquRsUtFxbQKub/8TMyYYGI0+k=
+github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
+github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
+github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
+github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/bool64/dev v0.2.35 h1:M17TLsO/pV2J7PYI/gpe3Ua26ETkzZGb+dC06eoMqlk=
+github.com/bool64/dev v0.2.35/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
+github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
+github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
+github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/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=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
+github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
+github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
+github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
+github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
+github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
+github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
+github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
+github.com/swaggest/jsonschema-go v0.3.72 h1:IHaGlR1bdBUBPfhe4tfacN2TGAPKENEGiNyNzvnVHv4=
+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/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=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
+github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
+go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
+go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08=
+go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
+golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
+golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
+modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
+modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
+modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
+modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/java-runtime/testdata/java/gomodule/server.go b/java-runtime/testdata/java/gomodule/server.go
new file mode 100644
index 0000000000..9adc032019
--- /dev/null
+++ b/java-runtime/testdata/java/gomodule/server.go
@@ -0,0 +1,157 @@
+package gomodule
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/TBD54566975/ftl/go-runtime/ftl"
+)
+
+type TestObject struct {
+ IntField int
+ FloatField float64
+ StringField string
+ BytesField []byte
+ BoolField bool
+ TimeField time.Time
+ ArrayField []string
+ MapField map[string]string
+}
+
+type TestObjectOptionalFields struct {
+ IntField ftl.Option[int]
+ FloatField ftl.Option[float64]
+ StringField ftl.Option[string]
+ BytesField ftl.Option[[]byte]
+ BoolField ftl.Option[bool]
+ TimeField ftl.Option[time.Time]
+ ArrayField ftl.Option[[]string]
+ MapField ftl.Option[map[string]string]
+}
+
+// Test different signatures
+
+//ftl:verb export
+func SourceVerb(ctx context.Context) (string, error) {
+ return "Source Verb", nil
+}
+
+//ftl:verb export
+func SinkVerb(ctx context.Context, req string) error {
+ return nil
+}
+
+//ftl:verb export
+func EmptyVerb(ctx context.Context) error {
+ return nil
+}
+
+//ftl:verb export
+func ErrorEmptyVerb(ctx context.Context) error {
+ return fmt.Errorf("verb failed")
+}
+
+// Test different param and return types
+
+//ftl:verb export
+func IntVerb(ctx context.Context, val int) (int, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func FloatVerb(ctx context.Context, val float64) (float64, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func StringVerb(ctx context.Context, val string) (string, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func BytesVerb(ctx context.Context, val []byte) ([]byte, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func BoolVerb(ctx context.Context, val bool) (bool, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func StringArrayVerb(ctx context.Context, val []string) ([]string, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func StringMapVerb(ctx context.Context, val map[string]string) (map[string]string, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func TimeVerb(ctx context.Context, val time.Time) (time.Time, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func TestObjectVerb(ctx context.Context, val TestObject) (TestObject, error) {
+ return val, nil
+}
+
+//ftl:verb export
+func TestObjectOptionalFieldsVerb(ctx context.Context, val TestObjectOptionalFields) (TestObjectOptionalFields, error) {
+ return val, nil
+}
+
+// Now optional versions of all of the above
+
+//ftl:verb export
+func OptionalIntVerb(ctx context.Context, val ftl.Option[int]) (ftl.Option[int], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalFloatVerb(ctx context.Context, val ftl.Option[float64]) (ftl.Option[float64], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalStringVerb(ctx context.Context, val ftl.Option[string]) (ftl.Option[string], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalBytesVerb(ctx context.Context, val ftl.Option[[]byte]) (ftl.Option[[]byte], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalBoolVerb(ctx context.Context, val ftl.Option[bool]) (ftl.Option[bool], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalStringArrayVerb(ctx context.Context, val ftl.Option[[]string]) (ftl.Option[[]string], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalStringMapVerb(ctx context.Context, val ftl.Option[map[string]string]) (ftl.Option[map[string]string], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalTimeVerb(ctx context.Context, val ftl.Option[time.Time]) (ftl.Option[time.Time], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalTestObjectVerb(ctx context.Context, val ftl.Option[TestObject]) (ftl.Option[TestObject], error) {
+ return val, nil
+}
+
+//ftl:verb export
+func OptionalTestObjectOptionalFieldsVerb(ctx context.Context, val ftl.Option[TestObjectOptionalFields]) (ftl.Option[TestObjectOptionalFields], error) {
+ return val, nil
+}
diff --git a/java-runtime/testdata/go/javamodule/ftl.toml b/java-runtime/testdata/java/javamodule/ftl.toml
similarity index 100%
rename from java-runtime/testdata/go/javamodule/ftl.toml
rename to java-runtime/testdata/java/javamodule/ftl.toml
diff --git a/java-runtime/testdata/go/javamodule/pom.xml b/java-runtime/testdata/java/javamodule/pom.xml
similarity index 98%
rename from java-runtime/testdata/go/javamodule/pom.xml
rename to java-runtime/testdata/java/javamodule/pom.xml
index 43d234296e..4027c0007e 100644
--- a/java-runtime/testdata/go/javamodule/pom.xml
+++ b/java-runtime/testdata/java/javamodule/pom.xml
@@ -3,7 +3,7 @@
4.0.0
xyz.block.ftl.examples
javamodule
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
1.0-SNAPSHOT
@@ -35,7 +35,7 @@
xyz.block
ftl-java-runtime
- 1.0.0-SNAPSHOT
+ 1.0-SNAPSHOT
io.quarkus
diff --git a/java-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/java/test/TestInvokeGo.java b/java-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/java/test/TestInvokeGo.java
new file mode 100644
index 0000000000..2a6e3ad884
--- /dev/null
+++ b/java-runtime/testdata/java/javamodule/src/main/java/xyz/block/ftl/java/test/TestInvokeGo.java
@@ -0,0 +1,187 @@
+package xyz.block.ftl.java.test;
+
+import ftl.gomodule.BoolVerbClient;
+import ftl.gomodule.BytesVerbClient;
+import ftl.gomodule.EmptyVerbClient;
+import ftl.gomodule.ErrorEmptyVerbClient;
+import ftl.gomodule.FloatVerbClient;
+import ftl.gomodule.IntVerbClient;
+import ftl.gomodule.OptionalBoolVerbClient;
+import ftl.gomodule.OptionalBytesVerbClient;
+import ftl.gomodule.OptionalFloatVerbClient;
+import ftl.gomodule.OptionalIntVerbClient;
+import ftl.gomodule.OptionalStringArrayVerbClient;
+import ftl.gomodule.OptionalStringMapVerbClient;
+import ftl.gomodule.OptionalStringVerbClient;
+import ftl.gomodule.OptionalTestObjectOptionalFieldsVerbClient;
+import ftl.gomodule.OptionalTestObjectVerbClient;
+import ftl.gomodule.OptionalTimeVerbClient;
+import ftl.gomodule.SinkVerbClient;
+import ftl.gomodule.SourceVerbClient;
+import ftl.gomodule.StringArrayVerbClient;
+import ftl.gomodule.StringMapVerbClient;
+import ftl.gomodule.StringVerbClient;
+import ftl.gomodule.TestObject;
+import ftl.gomodule.TestObjectOptionalFields;
+import ftl.gomodule.TestObjectOptionalFieldsVerbClient;
+import ftl.gomodule.TestObjectVerbClient;
+import ftl.gomodule.TimeVerbClient;
+import org.jetbrains.annotations.NotNull;
+import xyz.block.ftl.Export;
+import xyz.block.ftl.Verb;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Map;
+
+public class TestInvokeGo {
+
+ @Export
+ @Verb
+ public void emptyVerb(EmptyVerbClient emptyVerbClient) {
+ emptyVerbClient.call();
+ }
+
+ @Export
+ @Verb
+ public void sinkVerb(String input, SinkVerbClient sinkVerbClient) {
+ sinkVerbClient.call(input);
+ }
+
+ @Export
+ @Verb
+ public String sourceVerb(SourceVerbClient sourceVerbClient) {
+ return sourceVerbClient.call();
+ }
+
+ @Export
+ @Verb
+ public void errorEmptyVerb(ErrorEmptyVerbClient client) {
+ client.call();
+ }
+
+ @Export
+ @Verb
+ public long intVerb(long val, IntVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public double floatVerb(double val, FloatVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public @NotNull String stringVerb(@NotNull String val, StringVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public byte[] bytesVerb(byte[] val, BytesVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public boolean boolVerb(boolean val, BoolVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public @NotNull List stringArrayVerb(@NotNull List val, StringArrayVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public @NotNull Map stringMapVerb(@NotNull Map val, StringMapVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public @NotNull ZonedDateTime timeVerb(@NotNull ZonedDateTime instant, TimeVerbClient client) {
+ return client.call(instant);
+ }
+
+ @Export
+ @Verb
+ public @NotNull TestObject testObjectVerb(@NotNull TestObject val, TestObjectVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public @NotNull TestObjectOptionalFields testObjectOptionalFieldsVerb(@NotNull TestObjectOptionalFields val, TestObjectOptionalFieldsVerbClient client) {
+ return client.call(val);
+ }
+
+ // now the same again but with option return / input types
+
+
+ @Export
+ @Verb
+ public Long optionalIntVerb(Long val, OptionalIntVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public Double optionalFloatVerb(Double val, OptionalFloatVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public String optionalStringVerb(String val, OptionalStringVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public byte[] optionalBytesVerb(byte[] val, OptionalBytesVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public boolean optionalBoolVerb(boolean val, OptionalBoolVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public List optionalStringArrayVerb(List val, OptionalStringArrayVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public Map optionalStringMapVerb(Map val, OptionalStringMapVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public ZonedDateTime optionalTimeVerb(ZonedDateTime instant, OptionalTimeVerbClient client) {
+ return client.call(instant);
+ }
+
+ @Export
+ @Verb
+ public TestObject optionalTestObjectVerb(TestObject val, OptionalTestObjectVerbClient client) {
+ return client.call(val);
+ }
+
+ @Export
+ @Verb
+ public TestObjectOptionalFields optionalTestObjectOptionalFieldsVerb(TestObjectOptionalFields val, OptionalTestObjectOptionalFieldsVerbClient client) {
+ return client.call(val);
+ }
+
+
+}