Skip to content

Commit

Permalink
feat: Added @asyncmessage
Browse files Browse the repository at this point in the history
Added support for a new `@AsyncMessage` annotation. This annotation can be used as a method parameter annotation on the same method the `@AsyncPublisher` or `@AsyncListener` annotations are used.

This new `@AsyncMessage` annotation allows to enrich the AsyncAPI Operation Message with all the supported fields.

The Message payload documentation stays in the Swagger `@Schema` annotation. This new `@AsyncMessage` annotation is intended to enrich the missing values.

A special case is the description, which is available in both. Any value defined in `@AsyncMessage` has higher priority than any other default value.
  • Loading branch information
Carlos Tasada committed Jun 26, 2023
1 parent 61352d7 commit c00d77f
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,50 @@ private Message buildMessage(OperationData operationData) {
String modelName = this.getSchemaService().register(payloadType);
String headerModelName = this.getSchemaService().register(operationData.getHeaders());

Schema schema = payloadType.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
/*
* Message information can be obtained via a @AsyncMessage annotation on the method parameter, the Payload
* itself or via the Swagger @Schema annotation on the Payload.
*/

var schema = payloadType.getAnnotation(Schema.class);
String description = schema != null ? schema.description() : null;

return Message.builder()
var builder = Message.builder()
.name(payloadType.getName())
.title(modelName)
.description(description)
.payload(PayloadReference.fromModelName(modelName))
.headers(HeaderReference.fromModelName(headerModelName))
.bindings(operationData.getMessageBinding())
.build();
.bindings(operationData.getMessageBinding());

// Retrieve the Message information obtained from the @AsyncMessage annotation. These values have higher priority
// so if we find them, we need to override the default values.
processAsyncMessageAnnotation(operationData.getMessage(), builder);

return builder.build();
}

private void processAsyncMessageAnnotation(Message annotationMessage, Message.MessageBuilder builder) {
if (annotationMessage != null) {
builder.messageId(annotationMessage.getMessageId());

var schemaFormat = annotationMessage.getSchemaFormat() != null ? annotationMessage.getSchemaFormat() : Message.DEFAULT_SCHEMA_FORMAT;
builder.schemaFormat(schemaFormat);

var annotationMessageDescription = annotationMessage.getDescription();
if (annotationMessageDescription != null && !annotationMessageDescription.isBlank()) {
builder.description(annotationMessageDescription);
}

var name = annotationMessage.getName();
if (name != null && !name.isBlank()) {
builder.name(annotationMessage.getName());
}

var title = annotationMessage.getTitle();
if (title != null && !title.isBlank()) {
builder.title(annotationMessage.getTitle());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import com.asyncapi.v2.binding.operation.OperationBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProcessedMessageBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProcessedOperationBinding;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaderSchema;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -79,4 +81,23 @@ public static Map<String, MessageBinding> processMessageBindingFromAnnotation(Me
.map(Optional::get)
.collect(Collectors.toMap(ProcessedMessageBinding::getType, ProcessedMessageBinding::getBinding));
}

public static Message processMessageFromAnnotation(Method method) {
var messageBuilder = Message.builder();

Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (Annotation[] annotations : parameterAnnotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof AsyncMessage asyncMessage) {
messageBuilder.description(asyncMessage.description());
messageBuilder.messageId(asyncMessage.messageId());
messageBuilder.name(asyncMessage.name());
messageBuilder.schemaFormat(asyncMessage.schemaFormat());
messageBuilder.title(asyncMessage.title());
}
}
}

return messageBuilder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner;
import io.github.stavshamir.springwolf.asyncapi.types.ConsumerData;
import io.github.stavshamir.springwolf.asyncapi.types.OperationData;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -71,14 +72,15 @@ private Stream<OperationData> toOperationData(Method method) {

Map<String, OperationBinding> operationBindings = AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors);
Map<String, MessageBinding> messageBindings = AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors);
Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method);

Class<AsyncListener> annotationClass = AsyncListener.class;
return Arrays
.stream(method.getAnnotationsByType(annotationClass))
.map(annotation -> toConsumerData(method, operationBindings, messageBindings, annotation));
.map(annotation -> toConsumerData(method, operationBindings, messageBindings, message, annotation));
}

private ConsumerData toConsumerData(Method method, Map<String, OperationBinding> operationBindings, Map<String, MessageBinding> messageBindings, AsyncListener annotation) {
private ConsumerData toConsumerData(Method method, Map<String, OperationBinding> operationBindings, Map<String, MessageBinding> messageBindings, Message message, AsyncListener annotation) {
AsyncOperation op = annotation.operation();
Class<?> payloadType = op.payloadType() != Object.class ? op.payloadType() :
SpringPayloadAnnotationTypeExtractor.getPayloadType(method);
Expand All @@ -89,6 +91,7 @@ private ConsumerData toConsumerData(Method method, Map<String, OperationBinding>
.payloadType(payloadType)
.operationBinding(operationBindings)
.messageBinding(messageBindings)
.message(message)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation;

import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;

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

import static io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message.DEFAULT_SCHEMA_FORMAT;

/**
* Annotation is mapped to {@link Message}
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface AsyncMessage {
/**
* Mapped to {@link Message#getDescription()}
*/
String description() default "";

/**
* Mapped to {@link Message#getMessageId()}
*/
String messageId() default "";

/**
* Mapped to {@link Message#getName()}
*/
String name() default "";

/**
* Mapped to {@link Message#getSchemaFormat()}
*/
String schemaFormat() default DEFAULT_SCHEMA_FORMAT;

/**
* Mapped to {@link Message#getTitle()}
*/
String title() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner;
import io.github.stavshamir.springwolf.asyncapi.types.OperationData;
import io.github.stavshamir.springwolf.asyncapi.types.ProducerData;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -71,14 +72,15 @@ private Stream<OperationData> toOperationData(Method method) {

Map<String, OperationBinding> operationBindings = AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors);
Map<String, MessageBinding> messageBindings = AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors);
Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method);

Class<AsyncPublisher> annotationClass = AsyncPublisher.class;
return Arrays
.stream(method.getAnnotationsByType(annotationClass))
.map(annotation -> toConsumerData(method, operationBindings, messageBindings, annotation));
.map(annotation -> toConsumerData(method, operationBindings, messageBindings, message, annotation));
}

private ProducerData toConsumerData(Method method, Map<String, OperationBinding> operationBindings, Map<String, MessageBinding> messageBindings, AsyncPublisher annotation) {
private ProducerData toConsumerData(Method method, Map<String, OperationBinding> operationBindings, Map<String, MessageBinding> messageBindings, Message message, AsyncPublisher annotation) {
AsyncOperation op = annotation.operation();
Class<?> payloadType = op.payloadType() != Object.class ? op.payloadType() :
SpringPayloadAnnotationTypeExtractor.getPayloadType(method);
Expand All @@ -89,6 +91,7 @@ private ProducerData toConsumerData(Method method, Map<String, OperationBinding>
.payloadType(payloadType)
.operationBinding(operationBindings)
.messageBinding(messageBindings)
.message(message)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.asyncapi.v2.binding.channel.ChannelBinding;
import com.asyncapi.v2.binding.message.MessageBinding;
import com.asyncapi.v2.binding.operation.OperationBinding;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -71,4 +72,9 @@ public class ConsumerData implements OperationData {
* </code>
*/
protected Map<String, ? extends MessageBinding> messageBinding;

/**
* Operation message.
*/
protected Message message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.asyncapi.v2.binding.channel.ChannelBinding;
import com.asyncapi.v2.binding.message.MessageBinding;
import com.asyncapi.v2.binding.operation.OperationBinding;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders;

import java.util.Map;
Expand Down Expand Up @@ -46,6 +47,8 @@ public interface OperationData {
*/
Map<String, ? extends MessageBinding> getMessageBinding();

Message getMessage();

enum OperationType {
PUBLISH("publish"), SUBSCRIBE("subscribe");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.asyncapi.v2.binding.channel.ChannelBinding;
import com.asyncapi.v2.binding.message.MessageBinding;
import com.asyncapi.v2.binding.operation.OperationBinding;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -72,4 +73,8 @@ public class ProducerData implements OperationData {
*/
protected Map<String, ? extends MessageBinding> messageBinding;

/**
* Operation message.
*/
protected Message message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,19 @@
@AllArgsConstructor
public class Message {

public static final String DEFAULT_SCHEMA_FORMAT = "application/vnd.oai.openapi+json;version=3.0.0";

@Builder.Default
private String schemaFormat = "application/vnd.oai.openapi+json;version=3.0.0";
private String schemaFormat = DEFAULT_SCHEMA_FORMAT;

/**
* Unique string used to identify the message.
* <p>
* The id MUST be unique among all messages described in the API. The messageId value is case-sensitive.
* Tools and libraries MAY use the messageId to uniquely identify a message, therefore, it is RECOMMENDED to
* follow common programming naming conventions.
*/
private String messageId;

/**
* A machine-friendly name for the message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ void scan_componentHasListenerMethod() {
.title(SimpleFoo.class.getSimpleName())
.description("SimpleFoo Message Description")
.payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName()))
.schemaFormat(Message.DEFAULT_SCHEMA_FORMAT)
.headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))
.bindings(EMPTY_MAP)
.build();
Expand Down Expand Up @@ -100,6 +101,7 @@ void scan_componentHasListenerMethodWithAllAttributes() {
.name(SimpleFoo.class.getName())
.title(SimpleFoo.class.getSimpleName())
.description("SimpleFoo Message Description")
.schemaFormat(Message.DEFAULT_SCHEMA_FORMAT)
.payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName()))
.headers(HeaderReference.fromModelName("TestSchema"))
.bindings(EMPTY_MAP)
Expand Down Expand Up @@ -134,6 +136,7 @@ void scan_componentHasMultipleListenerAnnotations() {
.name(SimpleFoo.class.getName())
.title(SimpleFoo.class.getSimpleName())
.payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName()))
.schemaFormat(Message.DEFAULT_SCHEMA_FORMAT)
.headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))
.bindings(EMPTY_MAP);

Expand Down Expand Up @@ -168,6 +171,41 @@ void scan_componentHasMultipleListenerAnnotations() {
"test-channel-2", expectedChannel2));
}

@Test
void scan_componentHasAsyncMethodAnnotation() {
// Given a class with methods annotated with AsyncListener, where only the channel-name is set
setClassToScan(ClassWithMessageAnnotation.class);

// When scan is called
Map<String, ChannelItem> actualChannels = channelScanner.scan();

// Then the returned collection contains the channel
Message message = Message.builder()
.messageId("simpleFoo")
.name("SimpleFooPayLoad")
.title("Message Title")
.description("Message description")
.payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName()))
.schemaFormat("application/schema+json;version=draft-07")
.headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))
.bindings(EMPTY_MAP)
.build();

Operation operation = Operation.builder()
.description("test channel operation description")
.operationId("test-channel_publish")
.bindings(EMPTY_MAP)
.message(message)
.build();

ChannelItem expectedChannel = ChannelItem.builder()
.bindings(null)
.publish(operation)
.build();

assertThat(actualChannels)
.containsExactly(Map.entry("test-channel", expectedChannel));
}

private static class ClassWithoutListenerAnnotation {

Expand Down Expand Up @@ -221,6 +259,27 @@ private void methodWithMultipleAnnotation(SimpleFoo payload) {
}
}

private static class ClassWithMessageAnnotation {

@AsyncListener(operation = @AsyncOperation(
channelName = "test-channel",
description = "test channel operation description"
))
private void methodWithAnnotation(
@AsyncMessage(
description = "Message description",
messageId = "simpleFoo",
name = "SimpleFooPayLoad",
schemaFormat = "application/schema+json;version=draft-07",
title = "Message Title"
) SimpleFoo payload) {
}

private void methodWithoutAnnotation() {
}

}

@Data
@NoArgsConstructor
@Schema(description = "SimpleFoo Message Description")
Expand Down

0 comments on commit c00d77f

Please sign in to comment.