xyz.block.ftl.runtime.VerbRegistry Maven / Gradle / Ivy
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;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import jakarta.inject.Singleton;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.ByteString;
import io.quarkus.arc.Arc;
import io.quarkus.arc.InstanceHandle;
import xyz.block.ftl.v1.CallRequest;
import xyz.block.ftl.v1.CallResponse;
@Singleton
public class VerbRegistry {
private static final Logger log = Logger.getLogger(VerbRegistry.class);
final ObjectMapper mapper;
private final Map verbs = new ConcurrentHashMap<>();
public VerbRegistry(ObjectMapper mapper) {
this.mapper = mapper;
}
public void register(String module, String name, InstanceHandle> verbHandlerClass, Method method,
List> paramMappers, boolean allowNullReturn) {
verbs.put(new Key(module, name), new AnnotatedEndpointHandler(verbHandlerClass, method, paramMappers, allowNullReturn));
}
public void register(String module, String name, VerbInvoker verbInvoker) {
verbs.put(new Key(module, name), verbInvoker);
}
public CallResponse invoke(CallRequest request) {
VerbInvoker handler = verbs.get(new Key(request.getVerb().getModule(), request.getVerb().getName()));
if (handler == null) {
return CallResponse.newBuilder().setError(CallResponse.Error.newBuilder().setMessage("Verb not found").build())
.build();
}
return handler.handle(request);
}
private record Key(String module, String name) {
}
private class AnnotatedEndpointHandler implements VerbInvoker {
final InstanceHandle> verbHandlerClass;
final Method method;
final List> parameterSuppliers;
final boolean allowNull;
private AnnotatedEndpointHandler(InstanceHandle> verbHandlerClass, Method method,
List> parameterSuppliers, boolean allowNull) {
this.verbHandlerClass = verbHandlerClass;
this.method = method;
this.parameterSuppliers = parameterSuppliers;
this.allowNull = allowNull;
}
public CallResponse handle(CallRequest in) {
try {
Object[] params = new Object[parameterSuppliers.size()];
for (int i = 0; i < parameterSuppliers.size(); i++) {
params[i] = parameterSuppliers.get(i).apply(mapper, in);
}
Object ret;
ret = method.invoke(verbHandlerClass.get(), params);
if (ret == null) {
if (allowNull) {
return CallResponse.newBuilder().setBody(ByteString.copyFrom("{}", StandardCharsets.UTF_8)).build();
} else {
return CallResponse.newBuilder().setError(
CallResponse.Error.newBuilder().setMessage("Verb returned an unexpected null response").build())
.build();
}
} else {
var mappedResponse = mapper.writer().writeValueAsBytes(ret);
return CallResponse.newBuilder().setBody(ByteString.copyFrom(mappedResponse)).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();
}
}
}
public record BodySupplier(Class> inputClass) implements BiFunction {
@Override
public Object apply(ObjectMapper mapper, CallRequest in) {
try {
return mapper.createParser(in.getBody().newInput()).readValueAs(inputClass);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static class SecretSupplier implements BiFunction, ParameterExtractor {
final String name;
final Class> inputClass;
public SecretSupplier(String name, Class> inputClass) {
this.name = name;
this.inputClass = inputClass;
}
@Override
public Object apply(ObjectMapper mapper, CallRequest in) {
var secret = FTLController.instance().getSecret(name);
try {
return mapper.createParser(secret).readValueAs(inputClass);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String getName() {
return name;
}
public Class> getInputClass() {
return inputClass;
}
@Override
public Object extractParameter(ResteasyReactiveRequestContext context) {
return apply(Arc.container().instance(ObjectMapper.class).get(), null);
}
}
public static class ConfigSupplier implements BiFunction, ParameterExtractor {
final String name;
final Class> inputClass;
public ConfigSupplier(String name, Class> inputClass) {
this.name = name;
this.inputClass = inputClass;
}
@Override
public Object apply(ObjectMapper mapper, CallRequest in) {
var secret = FTLController.instance().getConfig(name);
try {
return mapper.createParser(secret).readValueAs(inputClass);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Object extractParameter(ResteasyReactiveRequestContext context) {
return apply(Arc.container().instance(ObjectMapper.class).get(), null);
}
public Class> getInputClass() {
return inputClass;
}
public String getName() {
return name;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy