Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle JVM deserialization of generic types #3836

Merged
merged 2 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/runner/pubsub/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestRetry(t *testing.T) {
checkConsumed("subscriber", "consumeButFailAndRetry", false, retriesPerCall+1, optional.Some("secondCall")),
checkPublished("subscriber", "consumeButFailAndRetryFailed", 2),

in.IfLanguage("go", checkConsumed("subscriber", "consumeFromDeadLetter", true, 2, optional.None[string]())),
checkConsumed("subscriber", "consumeFromDeadLetter", true, 2, optional.None[string]()),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Pattern;

Expand All @@ -36,7 +35,6 @@

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.arc.processor.DotNames;
import xyz.block.ftl.Config;
Expand Down Expand Up @@ -73,7 +71,6 @@
import xyz.block.ftl.schema.v1.TypeAlias;
import xyz.block.ftl.schema.v1.Unit;
import xyz.block.ftl.schema.v1.Verb;
import xyz.block.ftl.v1.CallRequest;

public class ModuleBuilder {

Expand Down Expand Up @@ -190,7 +187,7 @@ public void registerVerbMethod(MethodInfo method, String className,
boolean exported, BodyType bodyType, Consumer<Verb.Builder> metadataCallback) {
try {
List<Class<?>> parameterTypes = new ArrayList<>();
List<BiFunction<ObjectMapper, CallRequest, Object>> paramMappers = new ArrayList<>();
List<VerbRegistry.ParameterSupplier> paramMappers = new ArrayList<>();
org.jboss.jandex.Type bodyParamType = null;
Nullability bodyParamNullability = Nullability.MISSING;

Expand All @@ -199,7 +196,9 @@ public void registerVerbMethod(MethodInfo method, String className,
MetadataCalls.Builder callsMetadata = MetadataCalls.newBuilder();
MetadataConfig.Builder configMetadata = MetadataConfig.newBuilder();
MetadataSecrets.Builder secretMetadata = MetadataSecrets.newBuilder();
var pos = -1;
for (var param : method.parameters()) {
pos++;
if (param.hasAnnotation(Secret.class)) {
Class<?> paramType = ModuleBuilder.loadClass(param.type());
parameterTypes.add(paramType);
Expand Down Expand Up @@ -248,7 +247,7 @@ public void registerVerbMethod(MethodInfo method, String className,
Class<?> paramType = ModuleBuilder.loadClass(param.type());
parameterTypes.add(paramType);
//TODO: map and list types
paramMappers.add(new VerbRegistry.BodySupplier(paramType));
paramMappers.add(new VerbRegistry.BodySupplier(pos));
} else {
throw new RuntimeException("Unknown parameter type " + param.type() + " on FTL method: "
+ method.declaringClass().name() + "." + method.name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.BiFunction;

import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor;
Expand All @@ -27,7 +26,7 @@ public class FTLRecorder {
public static final String X_FTL_VERB = "X-ftl-verb";

public void registerVerb(String module, String verbName, String methodName, List<Class<?>> parameterTypes,
Class<?> verbHandlerClass, List<BiFunction<ObjectMapper, CallRequest, Object>> paramMappers,
Class<?> verbHandlerClass, List<VerbRegistry.ParameterSupplier> paramMappers,
boolean allowNullReturn) {
//TODO: this sucks
try {
Expand Down Expand Up @@ -67,11 +66,11 @@ public void registerEnum(Class<?> ennum, List<Class<?>> variants) {
}
}

public BiFunction<ObjectMapper, CallRequest, Object> topicSupplier(String className, String callingVerb) {
public VerbRegistry.ParameterSupplier topicSupplier(String className, String callingVerb) {
try {
var cls = Thread.currentThread().getContextClassLoader().loadClass(className.replace("/", "."));
var topic = cls.getDeclaredConstructor(String.class).newInstance(callingVerb);
return new BiFunction<ObjectMapper, CallRequest, Object>() {
return new VerbRegistry.ParameterSupplier() {
@Override
public Object apply(ObjectMapper mapper, CallRequest callRequest) {
return topic;
Expand All @@ -82,11 +81,11 @@ public Object apply(ObjectMapper mapper, CallRequest callRequest) {
}
}

public BiFunction<ObjectMapper, CallRequest, Object> verbClientSupplier(String className) {
public VerbRegistry.ParameterSupplier verbClientSupplier(String className) {
try {
var cls = Thread.currentThread().getContextClassLoader().loadClass(className.replace("/", "."));
var client = cls.getDeclaredConstructor().newInstance();
return new BiFunction<ObjectMapper, CallRequest, Object>() {
return new VerbRegistry.ParameterSupplier() {
@Override
public Object apply(ObjectMapper mapper, CallRequest callRequest) {
return client;
Expand All @@ -97,8 +96,8 @@ public Object apply(ObjectMapper mapper, CallRequest callRequest) {
}
}

public BiFunction<ObjectMapper, CallRequest, Object> leaseClientSupplier() {
return new BiFunction<ObjectMapper, CallRequest, Object>() {
public VerbRegistry.ParameterSupplier leaseClientSupplier() {
return new VerbRegistry.ParameterSupplier() {

@Override
public Object apply(ObjectMapper mapper, CallRequest callRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
Expand All @@ -16,6 +17,7 @@
import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.google.protobuf.ByteString;

import io.quarkus.arc.Arc;
Expand All @@ -37,7 +39,7 @@ public VerbRegistry(ObjectMapper mapper) {
}

public void register(String module, String name, InstanceHandle<?> verbHandlerClass, Method method,
List<BiFunction<ObjectMapper, CallRequest, Object>> paramMappers, boolean allowNullReturn) {
List<ParameterSupplier> paramMappers, boolean allowNullReturn) {
verbs.put(new Key(module, name), new AnnotatedEndpointHandler(verbHandlerClass, method, paramMappers, allowNullReturn));
}

Expand All @@ -61,15 +63,18 @@ private record Key(String module, String name) {
private class AnnotatedEndpointHandler implements VerbInvoker {
final InstanceHandle<?> verbHandlerClass;
final Method method;
final List<BiFunction<ObjectMapper, CallRequest, Object>> parameterSuppliers;
final List<ParameterSupplier> parameterSuppliers;
final boolean allowNull;

private AnnotatedEndpointHandler(InstanceHandle<?> verbHandlerClass, Method method,
List<BiFunction<ObjectMapper, CallRequest, Object>> parameterSuppliers, boolean allowNull) {
List<ParameterSupplier> parameterSuppliers, boolean allowNull) {
this.verbHandlerClass = verbHandlerClass;
this.method = method;
this.parameterSuppliers = parameterSuppliers;
this.allowNull = allowNull;
for (ParameterSupplier parameterSupplier : parameterSuppliers) {
parameterSupplier.init(method);
}
}

public CallResponse handle(CallRequest in) {
Expand Down Expand Up @@ -106,19 +111,36 @@ public CallResponse handle(CallRequest in) {
}
}

public record BodySupplier(Class<?> inputClass) implements BiFunction<ObjectMapper, CallRequest, Object> {
public static class BodySupplier implements ParameterSupplier {

final int parameterIndex;
volatile Type inputClass;

public BodySupplier(int parameterIndex) {
this.parameterIndex = parameterIndex;
}

public void init(Method method) {
inputClass = method.getGenericParameterTypes()[parameterIndex];
}

@Override
public Object apply(ObjectMapper mapper, CallRequest in) {
try {
return mapper.createParser(in.getBody().newInput()).readValueAs(inputClass);
ObjectReader reader = mapper.reader();
return reader.forType(reader.getTypeFactory().constructType(inputClass))
.readValue(in.getBody().newInput());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public int getParameterIndex() {
return parameterIndex;
}
}

public static class SecretSupplier implements BiFunction<ObjectMapper, CallRequest, Object>, ParameterExtractor {
public static class SecretSupplier implements ParameterSupplier, ParameterExtractor {

final String name;
final Class<?> inputClass;
Expand Down Expand Up @@ -153,7 +175,7 @@ public Object extractParameter(ResteasyReactiveRequestContext context) {
}
}

public static class ConfigSupplier implements BiFunction<ObjectMapper, CallRequest, Object>, ParameterExtractor {
public static class ConfigSupplier implements ParameterSupplier, ParameterExtractor {

final String name;
final Class<?> inputClass;
Expand Down Expand Up @@ -187,4 +209,12 @@ public String getName() {
}
}

public interface ParameterSupplier extends BiFunction<ObjectMapper, CallRequest, Object> {

// TODO: this is pretty yuck, but it lets us avoid a whole heap of nasty stuff to get the generic type
default void init(Method method) {

}

}
}
6 changes: 6 additions & 0 deletions jvm-runtime/jvm_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func TestJVMCoreFunctionality(t *testing.T) {
})
})...)
tests = append(tests, AllRuntimesVerbTest("testObjectVerb", exampleObject)...)
tests = append(tests, AllRuntimesVerbTest("testGenericType", FailedEvent[TestObject]{Event: exampleObject, Error: "failed"})...)
tests = append(tests, AllRuntimesVerbTest("testObjectOptionalFieldsVerb", exampleOptionalFieldsObject)...)
tests = append(tests, AllRuntimesVerbTest("objectMapVerb", map[string]TestObject{"hello": exampleObject})...)
tests = append(tests, AllRuntimesVerbTest("objectArrayVerb", []TestObject{exampleObject})...)
Expand Down Expand Up @@ -480,3 +481,8 @@ type Thing struct{}

func (Word) tag() {}
func (Thing) tag() {}

type FailedEvent[Event any] struct {
Event Event `json:"event"`
Error string `json:"error"`
}
7 changes: 7 additions & 0 deletions jvm-runtime/testdata/go/gomodule/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"time"

"ftl/builtin"

"github.com/decentralized-identity/web5-go/dids/did"

"github.com/block/ftl/go-runtime/ftl"
Expand Down Expand Up @@ -219,6 +221,11 @@ func TestObjectVerb(ctx context.Context, val TestObject) (TestObject, error) {
return val, nil
}

//ftl:verb export
func TestGenericType(ctx context.Context, val builtin.FailedEvent[TestObject]) (builtin.FailedEvent[TestObject], error) {
return val, nil
}

//ftl:verb export
func TestObjectOptionalFieldsVerb(ctx context.Context, val TestObjectOptionalFields) (TestObjectOptionalFields, error) {
return val, nil
Expand Down
8 changes: 7 additions & 1 deletion jvm-runtime/testdata/go/gomodule/types.ftl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,8 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import ftl.gomodule.AnimalWrapper;
import ftl.gomodule.BoolVerbClient;
import ftl.gomodule.BytesVerbClient;
import ftl.gomodule.ColorWrapper;
import ftl.gomodule.EmptyVerbClient;
import ftl.gomodule.ErrorEmptyVerbClient;
import ftl.gomodule.ExternalTypeVerbClient;
import ftl.gomodule.FloatVerbClient;
import ftl.gomodule.IntVerbClient;
import ftl.gomodule.ObjectArrayVerbClient;
import ftl.gomodule.ObjectMapVerbClient;
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.ParameterizedObjectVerbClient;
import ftl.gomodule.ParameterizedType;
import ftl.gomodule.Scalar;
import ftl.gomodule.ShapeWrapper;
import ftl.gomodule.SinkVerbClient;
import ftl.gomodule.SourceVerbClient;
import ftl.gomodule.StringArrayVerbClient;
import ftl.gomodule.StringEnumVerbClient;
import ftl.gomodule.StringList;
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 ftl.gomodule.TypeEnumVerbClient;
import ftl.gomodule.TypeEnumWrapper;
import ftl.gomodule.TypeWrapperEnumVerbClient;
import ftl.gomodule.ValueEnumVerbClient;
import ftl.builtin.FailedEvent;
import ftl.gomodule.*;
import web5.sdk.dids.didcore.Did;
import xyz.block.ftl.Export;
import xyz.block.ftl.Verb;
Expand Down Expand Up @@ -154,6 +115,13 @@ public boolean boolVerb(boolean val, BoolVerbClient client) {
return client.testObjectVerb(val);
}

@Export
@Verb
public @NotNull FailedEvent<TestObject> testGenericType(@NotNull FailedEvent<TestObject> val,
TestGenericTypeClient client) {
return client.testGenericType(val);
}

@Export
@Verb
public @NotNull TestObjectOptionalFields testObjectOptionalFieldsVerb(@NotNull TestObjectOptionalFields val,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package xyz.block.ftl.test

import ftl.builtin.FailedEvent
import ftl.gomodule.*
import web5.sdk.dids.didcore.Did
import xyz.block.ftl.Export
Expand Down Expand Up @@ -117,6 +118,12 @@ fun testObjectOptionalFieldsVerb(
return client.testObjectOptionalFieldsVerb(payload)
}

@Export
@Verb
fun testGenericType(payload: FailedEvent<TestObject>, client: TestGenericTypeClient): FailedEvent<TestObject> {
return client.testGenericType(payload)
}

// now the same again but with option return / input types
@Export
@Verb
Expand Down
Loading