Skip to content

Commit

Permalink
feat: support for subscription meta annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas committed Aug 12, 2024
1 parent 12c1679 commit 13c8ca7
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package xyz.block.ftl.deployment;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
Expand Down Expand Up @@ -33,6 +34,7 @@
import io.quarkus.bootstrap.prebuild.CodeGenException;
import io.quarkus.deployment.CodeGenContext;
import io.quarkus.deployment.CodeGenProvider;
import xyz.block.ftl.Subscription;
import xyz.block.ftl.VerbClient;
import xyz.block.ftl.VerbClientDefinition;
import xyz.block.ftl.VerbClientEmpty;
Expand Down Expand Up @@ -143,6 +145,9 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {

} else if (decl.hasData()) {
var data = decl.getData();
if (!data.getExport()) {
continue;
}
String thisType = className(data.getName());
TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType)
.addModifiers(Modifier.PUBLIC);
Expand Down Expand Up @@ -200,6 +205,9 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {

} else if (decl.hasEnum()) {
var data = decl.getEnum();
if (!data.getExport()) {
continue;
}
String thisType = className(data.getName());
TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType)
.addModifiers(Modifier.PUBLIC);
Expand All @@ -213,6 +221,32 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {

javaFile.writeTo(context.outDir());

} else if (decl.hasTopic()) {
var data = decl.getTopic();
if (!data.getExport()) {
continue;
}
String thisType = className(data.getName() + "Subscription");

TypeSpec.Builder dataBuilder = TypeSpec.annotationBuilder(thisType)
.addModifiers(Modifier.PUBLIC);
if (data.getEvent().hasRef()) {
dataBuilder.addJavadoc("Subscription to the topic of type {@link $L}",
data.getEvent().getRef().getName());
}
dataBuilder.addAnnotation(AnnotationSpec.builder(Retention.class)
.addMember("value", "java.lang.annotation.RetentionPolicy.RUNTIME").build());
dataBuilder.addAnnotation(AnnotationSpec.builder(Subscription.class)
.addMember("topic", "\"" + data.getName() + "\"")
.addMember("module", "\"" + module.getName() + "\"")
.addMember("name", "\"" + data.getName() + "Subscription\"")
.build());

JavaFile javaFile = JavaFile.builder(packageName, dataBuilder.build())
.build();

javaFile.writeTo(context.outDir());

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@
import java.util.stream.Collectors;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.VoidType;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.common.model.MethodParameter;
import org.jboss.resteasy.reactive.common.model.ParameterType;
import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor;
import org.jboss.resteasy.reactive.server.mapping.URITemplate;
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;
import org.jetbrains.annotations.NotNull;

import com.fasterxml.jackson.databind.ObjectMapper;

Expand Down Expand Up @@ -63,6 +66,7 @@
import xyz.block.ftl.Secret;
import xyz.block.ftl.Subscription;
import xyz.block.ftl.Verb;
import xyz.block.ftl.VerbName;
import xyz.block.ftl.runtime.FTLController;
import xyz.block.ftl.runtime.FTLHttpHandler;
import xyz.block.ftl.runtime.FTLRecorder;
Expand Down Expand Up @@ -97,6 +101,8 @@

class FtlProcessor {

private static final Logger log = Logger.getLogger(FtlProcessor.class);

private static final String SCHEMA_OUT = "schema.pb";
private static final String FEATURE = "ftl-java-runtime";
public static final DotName EXPORT = DotName.createSimple(Export.class);
Expand Down Expand Up @@ -198,7 +204,8 @@ public void registerVerbs(CombinedIndexBuildItem index,
ResteasyReactiveResourceMethodEntriesBuildItem restEndpoints,
TopicsBuildItem topics,
VerbClientBuildItem verbClients,
ModuleNameBuildItem moduleNameBuildItem) throws Exception {
ModuleNameBuildItem moduleNameBuildItem,
SubscriptionMetaAnnotationsBuildItem subscriptionMetaAnnotationsBuildItem) throws Exception {
String moduleName = moduleNameBuildItem.getModuleName();
Module.Builder moduleBuilder = Module.newBuilder()
.setName(moduleName)
Expand Down Expand Up @@ -235,23 +242,36 @@ public void registerVerbs(CombinedIndexBuildItem index,
}));
}
for (var subscription : index.getIndex().getAnnotations(SUBSCRIPTION)) {
if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) {
continue;
}
var method = subscription.target().asMethod();
String className = method.declaringClass().name().toString();
String name = subscription.value("name").asString();
String module = subscription.value("module") == null ? moduleName : subscription.value("module").asString();
String topic = subscription.value("topic").asString();
beans.addBeanClass(className);
moduleBuilder.addDecls(Decl.newBuilder().setSubscription(xyz.block.ftl.v1.schema.Subscription.newBuilder()
.setName(name).setTopic(Ref.newBuilder().setName(topic).setModule(module).build())).build());
handleVerbMethod(extractionContext, method, className, false, BodyType.REQUIRED, (builder -> {
builder.addMetadata(Metadata.newBuilder().setSubscriber(MetadataSubscriber.newBuilder().setName(name)));
}));
generateSubscription(moduleBuilder, extractionContext, beans, method, className, name, module, topic);
}
for (var metaSub : subscriptionMetaAnnotationsBuildItem.getAnnotations().entrySet()) {
for (var subscription : index.getIndex().getAnnotations(metaSub.getKey())) {
if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) {
log.warnf("Subscription annotation on non-method target: %s", subscription.target());
continue;
}
var method = subscription.target().asMethod();
generateSubscription(moduleBuilder, extractionContext, beans, method,
method.declaringClass().name().toString(),
metaSub.getValue().name(),
metaSub.getValue().module(),
metaSub.getValue().topic());
}

}

//TODO: make this composable so it is not just one big method, build items should contribute to the schema
for (var endpoint : restEndpoints.getEntries()) {
//TODO: naming
var verbName = endpoint.getMethodInfo().name();
var verbName = methodToName(endpoint.getMethodInfo());
recorder.registerHttpIngress(moduleName, verbName);

//TODO: handle type parameters properly
Expand Down Expand Up @@ -283,7 +303,8 @@ public void registerVerbs(CombinedIndexBuildItem index,
if (i.type == URITemplate.Type.CUSTOM_REGEX) {
throw new RuntimeException(
"Invalid path " + path + " on HTTP endpoint: " + endpoint.getActualClassInfo().name() + "."
+ endpoint.getMethodInfo().name() + " FTL does not support custom regular expressions");
+ methodToName(endpoint.getMethodInfo())
+ " FTL does not support custom regular expressions");
} else if (i.type == URITemplate.Type.LITERAL) {
if (i.literalText.equals("/")) {
continue;
Expand Down Expand Up @@ -348,14 +369,25 @@ public void registerVerbs(CombinedIndexBuildItem index,
Files.setPosixFilePermissions(output, newPerms);
}

private void generateSubscription(Module.Builder moduleBuilder, ExtractionContext extractionContext,
AdditionalBeanBuildItem.Builder beans, MethodInfo method, String className, String name, String module,
String topic) {
beans.addBeanClass(className);
moduleBuilder.addDecls(Decl.newBuilder().setSubscription(xyz.block.ftl.v1.schema.Subscription.newBuilder()
.setName(name).setTopic(Ref.newBuilder().setName(topic).setModule(module).build())).build());
handleVerbMethod(extractionContext, method, className, false, BodyType.REQUIRED, (builder -> {
builder.addMetadata(Metadata.newBuilder().setSubscriber(MetadataSubscriber.newBuilder().setName(name)));
}));
}

private void handleVerbMethod(ExtractionContext context, MethodInfo method, String className,
boolean exported, BodyType bodyType, Consumer<xyz.block.ftl.v1.schema.Verb.Builder> metadataCallback) {
try {
List<Class<?>> parameterTypes = new ArrayList<>();
List<BiFunction<ObjectMapper, CallRequest, Object>> paramMappers = new ArrayList<>();
org.jboss.jandex.Type bodyParamType = null;
xyz.block.ftl.v1.schema.Verb.Builder verbBuilder = xyz.block.ftl.v1.schema.Verb.newBuilder();
String verbName = method.name();
String verbName = methodToName(method);
MetadataCalls.Builder callsMetadata = MetadataCalls.newBuilder();
for (var param : method.parameters()) {
if (param.hasAnnotation(Secret.class)) {
Expand Down Expand Up @@ -407,7 +439,7 @@ private void handleVerbMethod(ExtractionContext context, MethodInfo method, Stri
bodyParamType = VoidType.VOID;
}

context.recorder.registerVerb(context.moduleName(), method.name(), method.name(), parameterTypes,
context.recorder.registerVerb(context.moduleName(), verbName, method.name(), parameterTypes,
Class.forName(className, false, Thread.currentThread().getContextClassLoader()), paramMappers);
verbBuilder
.setName(verbName)
Expand All @@ -429,6 +461,13 @@ private void handleVerbMethod(ExtractionContext context, MethodInfo method, Stri
}
}

private static @NotNull String methodToName(MethodInfo method) {
if (method.hasAnnotation(VerbName.class)) {
return method.annotation(VerbName.class).value().asString();
}
return method.name();
}

private static Class<?> loadClass(org.jboss.jandex.Type param) throws ClassNotFoundException {
if (param.kind() == org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE) {
return Class.forName(param.asParameterizedType().name().toString(), false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package xyz.block.ftl.deployment;

import java.util.Map;

import org.jboss.jandex.DotName;

import io.quarkus.builder.item.SimpleBuildItem;

public final class SubscriptionMetaAnnotationsBuildItem extends SimpleBuildItem {

private final Map<DotName, SubscriptionAnnotation> annotations;

public SubscriptionMetaAnnotationsBuildItem(Map<DotName, SubscriptionAnnotation> annotations) {
this.annotations = annotations;
}

public Map<DotName, SubscriptionAnnotation> getAnnotations() {
return annotations;
}

public record SubscriptionAnnotation(String module, String topic, String name) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.util.Map;
import java.util.Set;

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;

Expand All @@ -16,6 +18,7 @@
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.MethodDescriptor;
import xyz.block.ftl.Export;
import xyz.block.ftl.Subscription;
import xyz.block.ftl.Topic;
import xyz.block.ftl.TopicDefinition;
import xyz.block.ftl.runtime.TopicHelper;
Expand Down Expand Up @@ -77,4 +80,23 @@ TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer<Generat
}
return new TopicsBuildItem(topics);
}

@BuildStep
SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildItem combinedIndexBuildItem,
ModuleNameBuildItem moduleNameBuildItem) {
Map<DotName, SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation> annotations = new HashMap<>();
for (var subscriptions : combinedIndexBuildItem.getComputingIndex().getAnnotations(Subscription.class)) {
if (subscriptions.target().kind() != AnnotationTarget.Kind.TYPE) {
continue;
}
AnnotationValue moduleValue = subscriptions.value("module");
annotations.put(subscriptions.target().asClass().name(),
new SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation(
moduleValue == null || moduleValue.asString().isEmpty() ? moduleNameBuildItem.getModuleName()
: moduleValue.asString(),
subscriptions.value("topic").asString(),
subscriptions.value("name").asString()));
}
return new SubscriptionMetaAnnotationsBuildItem(annotations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Subscription {
/**
* @return The module of the topic to subscribe to, if empty then the topic is assumed to be in the current module.
Expand All @@ -25,11 +25,4 @@
*/
String name();

/**
* The type of the payload, if not set then it is inferred. This is mostly useful in the case where this is being
* used as a meta annotation, as it allows the processor to easily validate that a subscription is being used correctly.
*
*/
Class<?> payloadType() default Object.class;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package xyz.block.ftl;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
* Used to override the name of a verb
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface VerbName {
String value();
}

0 comments on commit 13c8ca7

Please sign in to comment.