diff --git a/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java b/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java index f8c4056b82..ac90e7bc4c 100644 --- a/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java +++ b/backend/controller/pubsub/testdata/java/publisher/src/main/java/xyz/block/ftl/java/test/publisher/Publisher.java @@ -4,6 +4,7 @@ import xyz.block.ftl.Export; import xyz.block.ftl.Topic; import xyz.block.ftl.TopicDefinition; +import xyz.block.ftl.Subscription; import xyz.block.ftl.Verb; public class Publisher { @@ -14,6 +15,11 @@ interface TestTopic extends Topic { } + @TopicDefinition("localTopic") + interface LocalTopic extends Topic { + + } + @Export @TopicDefinition("topic2") interface Topic2 extends Topic { @@ -21,7 +27,7 @@ interface Topic2 extends Topic { } @Verb - void publishTen(TestTopic testTopic) throws Exception { + void publishTen(LocalTopic testTopic) throws Exception { for (var i = 0; i < 10; ++i) { var t = java.time.ZonedDateTime.now(); Log.infof("Publishing %s", t); @@ -42,4 +48,9 @@ void publishOneToTopic2(Topic2 topic2) throws Exception { Log.infof("Publishing %s", t); topic2.publish(new PubSubEvent().setTime(t)); } + + @Subscription(topicClass = LocalTopic.class, name = "localSubscription") + public void local(TestTopic testTopic, PubSubEvent event) { + testTopic.publish(event); + } } diff --git a/docs/content/docs/reference/pubsub.md b/docs/content/docs/reference/pubsub.md index c1e5b464e1..77a6b02080 100644 --- a/docs/content/docs/reference/pubsub.md +++ b/docs/content/docs/reference/pubsub.md @@ -116,14 +116,13 @@ There are two ways to subscribe to a topic. The first is to declare a method wit subscribing to a topic inside the same module: ```java -@Subscription(topic = "invoices", name = "invoicesSubscription") +@Subscription(topicClass = InvoiceTopic.class, name = "invoicesSubscription") public void consumeInvoice(Invoice event) { // ... } ``` -This is ok, but it requires the use of string constants for the topic name, which can be error-prone. If you are subscribing to a topic from -another module, FTL will generate a type-safe subscription meta annotation you can use to subscribe to the topic: +If you are subscribing to a topic from another module, FTL will generate a type-safe subscription meta annotation you can use to subscribe to the topic: ```java @Retention(java.lang.annotation.RetentionPolicy.RUNTIME) diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java index 42de5a1d6e..8cece716d2 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java @@ -8,6 +8,7 @@ import xyz.block.ftl.LeaseClient; import xyz.block.ftl.Secret; import xyz.block.ftl.Subscription; +import xyz.block.ftl.TopicDefinition; import xyz.block.ftl.TypeAlias; import xyz.block.ftl.TypeAliasMapper; import xyz.block.ftl.Verb; @@ -27,4 +28,5 @@ private FTLDotNames() { public static final DotName TYPE_ALIAS = DotName.createSimple(TypeAlias.class); public static final DotName SUBSCRIPTION = DotName.createSimple(Subscription.class); public static final DotName LEASE_CLIENT = DotName.createSimple(LeaseClient.class); + public static final DotName TOPIC_DEFINITION = DotName.createSimple(TopicDefinition.class); } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java index 83e2d35cf5..5cc950d072 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionMetaAnnotationsBuildItem.java @@ -5,8 +5,10 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import io.quarkus.builder.item.SimpleBuildItem; +import xyz.block.ftl.Topic; public final class SubscriptionMetaAnnotationsBuildItem extends SimpleBuildItem { @@ -23,13 +25,33 @@ public Map getAnnotations() { public record SubscriptionAnnotation(String module, String topic, String name) { } - public static SubscriptionAnnotation fromJandex(AnnotationInstance subscriptions, String currentModuleName) { - AnnotationValue moduleValue = subscriptions.value("module"); + public static SubscriptionAnnotation fromJandex(IndexView indexView, AnnotationInstance subscriptions, + String currentModuleName) { + AnnotationValue moduleValue = subscriptions.value("module"); + AnnotationValue topicValue = subscriptions.value("topic"); + AnnotationValue topicClassValue = subscriptions.value("topicClass"); + String topicName; + if (topicValue != null && !topicValue.asString().isEmpty()) { + if (topicClassValue != null && !topicClassValue.asClass().name().toString().equals(Topic.class.getName())) { + throw new IllegalArgumentException("Cannot specify both topic and topicClass"); + } + topicName = topicValue.asString(); + } else if (topicClassValue != null && !topicClassValue.asClass().name().toString().equals(Topic.class.getName())) { + var topicClass = indexView.getClassByName(topicClassValue.asClass().name()); + AnnotationInstance annotation = topicClass.annotation(FTLDotNames.TOPIC_DEFINITION); + if (annotation == null) { + throw new IllegalArgumentException( + "topicClass must be annotated with @TopicDefinition for subscription " + subscriptions); + } + topicName = annotation.value().asString(); + } else { + throw new IllegalArgumentException("Either topic or topicClass must be specified on " + subscriptions); + } return new SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation( moduleValue == null || moduleValue.asString().isEmpty() ? currentModuleName : moduleValue.asString(), - subscriptions.value("topic").asString(), + topicName, subscriptions.value("name").asString()); } } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java index a2214461c2..415be5547b 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java @@ -33,7 +33,8 @@ SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildI continue; } annotations.put(subscriptions.target().asClass().name(), - SubscriptionMetaAnnotationsBuildItem.fromJandex(subscriptions, moduleNameBuildItem.getModuleName())); + SubscriptionMetaAnnotationsBuildItem.fromJandex(combinedIndexBuildItem.getComputingIndex(), subscriptions, + moduleNameBuildItem.getModuleName())); } return new SubscriptionMetaAnnotationsBuildItem(annotations); } @@ -47,7 +48,7 @@ public void registerSubscriptions(CombinedIndexBuildItem index, AdditionalBeanBuildItem.Builder beans = AdditionalBeanBuildItem.builder().setUnremovable(); var moduleName = moduleNameBuildItem.getModuleName(); for (var subscription : index.getIndex().getAnnotations(FTLDotNames.SUBSCRIPTION)) { - var info = SubscriptionMetaAnnotationsBuildItem.fromJandex(subscription, moduleName); + var info = SubscriptionMetaAnnotationsBuildItem.fromJandex(index.getComputingIndex(), subscription, moduleName); if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { continue; } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java index cc0d51faa3..3ad7ee4a24 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java @@ -18,7 +18,6 @@ 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; @@ -28,7 +27,7 @@ public class TopicsProcessor { @BuildStep TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer generatedTopicProducer) { - var topicDefinitions = index.getComputingIndex().getAnnotations(TopicDefinition.class); + var topicDefinitions = index.getComputingIndex().getAnnotations(FTLDotNames.TOPIC_DEFINITION); Map topics = new HashMap<>(); Set names = new HashSet<>(); for (var topicDefinition : topicDefinitions) { diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java index 9b2283f5e0..7ecd4c5d84 100644 --- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java +++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/Subscription.java @@ -15,9 +15,9 @@ /** * - * @return The name of the topic to subscribe to. + * @return The name of the topic to subscribe to. Cannot be used in conjunction with {@link #topicClass()}. */ - String topic(); + String topic() default ""; /** * @@ -25,4 +25,8 @@ */ String name(); + /** + * The class of the topic to subscribe to, which can be used in place of directly specifying the topic name and module. + */ + Class topicClass() default Topic.class; }