xyz.block.ftl.deployment.TopicsProcessor Maven / Gradle / Ivy
package xyz.block.ftl.deployment;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.MethodDescriptor;
import xyz.block.ftl.Export;
import xyz.block.ftl.Topic;
import xyz.block.ftl.TopicDefinition;
import xyz.block.ftl.runtime.TopicHelper;
import xyz.block.ftl.v1.schema.Decl;
public class TopicsProcessor {
public static final DotName TOPIC = DotName.createSimple(Topic.class);
@BuildStep
TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer generatedTopicProducer) {
var topicDefinitions = index.getComputingIndex().getAnnotations(TopicDefinition.class);
Map topics = new HashMap<>();
Set names = new HashSet<>();
for (var topicDefinition : topicDefinitions) {
var iface = topicDefinition.target().asClass();
if (!iface.isInterface()) {
throw new RuntimeException(
"@TopicDefinition can only be applied to interfaces " + iface.name() + " is not an interface");
}
Type paramType = null;
for (var i : iface.interfaceTypes()) {
if (i.name().equals(TOPIC)) {
if (i.kind() == Type.Kind.PARAMETERIZED_TYPE) {
paramType = i.asParameterizedType().arguments().get(0);
}
}
}
if (paramType == null) {
throw new RuntimeException("@TopicDefinition can only be applied to interfaces that directly extend " + TOPIC
+ " with a concrete type parameter " + iface.name() + " does not extend this interface");
}
String name = topicDefinition.value().asString();
if (names.contains(name)) {
throw new RuntimeException("Multiple topic definitions found for topic " + name);
}
names.add(name);
try (ClassCreator cc = new ClassCreator(new GeneratedClassGizmoAdaptor(generatedTopicProducer, true),
iface.name().toString() + "_fit_topic", null, Object.class.getName(), iface.name().toString())) {
var verb = cc.getFieldCreator("verb", String.class);
var constructor = cc.getConstructorCreator(String.class);
constructor.invokeSpecialMethod(MethodDescriptor.ofMethod(Object.class, "", void.class),
constructor.getThis());
constructor.writeInstanceField(verb.getFieldDescriptor(), constructor.getThis(), constructor.getMethodParam(0));
constructor.returnVoid();
var publish = cc.getMethodCreator("publish", void.class, Object.class);
var helper = publish
.invokeStaticMethod(MethodDescriptor.ofMethod(TopicHelper.class, "instance", TopicHelper.class));
publish.invokeVirtualMethod(
MethodDescriptor.ofMethod(TopicHelper.class, "publish", void.class, String.class, String.class,
Object.class),
helper, publish.load(name), publish.readInstanceField(verb.getFieldDescriptor(), publish.getThis()),
publish.getMethodParam(0));
publish.returnVoid();
topics.put(iface.name(), new TopicsBuildItem.DiscoveredTopic(name, cc.getClassName(), paramType,
iface.hasAnnotation(Export.class)));
}
}
return new TopicsBuildItem(topics);
}
@BuildStep
public SchemaContributorBuildItem topicSchema(TopicsBuildItem topics) {
//register all the topics we are defining in the module definition
return new SchemaContributorBuildItem(new Consumer() {
@Override
public void accept(ModuleBuilder moduleBuilder) {
for (var topic : topics.getTopics().values()) {
moduleBuilder.addDecls(Decl.newBuilder().setTopic(xyz.block.ftl.v1.schema.Topic.newBuilder()
.setExport(topic.exported())
.setName(topic.topicName())
.setEvent(moduleBuilder.buildType(topic.eventType(), topic.exported())).build()).build());
}
}
});
}
}