From 18abcbcba52c30dae59f2d96b3a537b56baa7313 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sat, 16 Dec 2023 19:24:34 +0100 Subject: [PATCH 1/2] refactor(core): replace asynclistener/publisher scanners with AsyncAnnotationChannelsScanner Uses composition instead of abstract classes --- .../SpringwolfScannerConfiguration.java | 55 ++- .../AsyncAnnotationChannelsScanner.java | 282 +++++++++++++ .../AsyncAnnotationScannerUtil.java | 5 +- .../annotation/AsyncListener.java | 2 + .../AsyncListenerAnnotationScanner.java | 113 ----- .../AsyncPublisherAnnotationScanner.java | 114 ----- ...syncAnnotationScannerIntegrationTest.java} | 103 +++-- .../AsyncAnnotationScannerUtilTest.java | 5 +- ...isherAnnotationScannerIntegrationTest.java | 397 ------------------ 9 files changed, 406 insertions(+), 670 deletions(-) create mode 100644 springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java rename springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/{operationdata => }/annotation/AsyncAnnotationScannerUtil.java (94%) delete mode 100644 springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java delete mode 100644 springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java rename springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/{operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java => annotation/AsyncAnnotationScannerIntegrationTest.java} (84%) rename springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/{operationdata => }/annotation/AsyncAnnotationScannerUtilTest.java (97%) delete mode 100644 springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java index e527b53f4..e3ad1e305 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java @@ -8,12 +8,15 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ConsumerOperationDataScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProducerOperationDataScanner; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListenerAnnotationScanner; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisherAnnotationScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncAnnotationChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ConfigurationClassScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.SpringwolfClassScanner; +import io.github.stavshamir.springwolf.asyncapi.types.OperationData; import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.schemas.SchemasService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -84,14 +87,15 @@ public ProducerOperationDataScanner producerOperationDataScanner( havingValue = "true", matchIfMissing = true) @Order(value = ChannelPriority.ASYNC_ANNOTATION) - public AsyncListenerAnnotationScanner asyncListenerAnnotationScanner( + public AsyncAnnotationChannelsScanner asyncListenerAnnotationScanner( SpringwolfClassScanner springwolfClassScanner, SchemasService schemasService, AsyncApiDocketService asyncApiDocketService, PayloadClassExtractor payloadClassExtractor, List operationBindingProcessors, List messageBindingProcessors) { - return new AsyncListenerAnnotationScanner( + return new AsyncAnnotationChannelsScanner<>( + buidAsyncListenerAnnotationProvider(), springwolfClassScanner, schemasService, asyncApiDocketService, @@ -106,14 +110,15 @@ public AsyncListenerAnnotationScanner asyncListenerAnnotationScanner( havingValue = "true", matchIfMissing = true) @Order(value = ChannelPriority.ASYNC_ANNOTATION) - public AsyncPublisherAnnotationScanner asyncPublisherAnnotationScanner( + public AsyncAnnotationChannelsScanner asyncPublisherAnnotationScanner( SpringwolfClassScanner springwolfClassScanner, SchemasService schemasService, AsyncApiDocketService asyncApiDocketService, PayloadClassExtractor payloadClassExtractor, List operationBindingProcessors, List messageBindingProcessors) { - return new AsyncPublisherAnnotationScanner( + return new AsyncAnnotationChannelsScanner<>( + buildAsyncPublisherAnnotationProvider(), springwolfClassScanner, schemasService, asyncApiDocketService, @@ -121,4 +126,42 @@ public AsyncPublisherAnnotationScanner asyncPublisherAnnotationScanner( operationBindingProcessors, messageBindingProcessors); } + + private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider buidAsyncListenerAnnotationProvider() { + return new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { + @Override + public Class getAnnotation() { + return AsyncListener.class; + } + + @Override + public AsyncOperation getAsyncOperation(AsyncListener annotation) { + return annotation.operation(); + } + + @Override + public OperationData.OperationType getOperationType() { + return OperationData.OperationType.PUBLISH; + } + }; + } + + private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider buildAsyncPublisherAnnotationProvider() { + return new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { + @Override + public Class getAnnotation() { + return AsyncPublisher.class; + } + + @Override + public AsyncOperation getAsyncOperation(AsyncPublisher annotation) { + return annotation.operation(); + } + + @Override + public OperationData.OperationType getOperationType() { + return OperationData.OperationType.SUBSCRIBE; + } + }; + } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java new file mode 100644 index 000000000..bd11e8e8a --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import com.asyncapi.v2._6_0.model.channel.ChannelItem; +import com.asyncapi.v2._6_0.model.channel.operation.Operation; +import com.asyncapi.v2._6_0.model.server.Server; +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.scanners.bindings.MessageBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; +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.asyncapi.types.channel.operation.message.PayloadReference; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.EmbeddedValueResolverAware; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessageObjectOrComposition; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +@Slf4j +@RequiredArgsConstructor +public class AsyncAnnotationChannelsScanner + implements ChannelsScanner, EmbeddedValueResolverAware { + + private final AsyncAnnotationProvider asyncAnnotationProvider; + private final ClassScanner classScanner; + private final SchemasService schemasService; + private final AsyncApiDocketService asyncApiDocketService; + private final PayloadClassExtractor payloadClassExtractor; + private final List operationBindingProcessors; + private final List messageBindingProcessors; + private StringValueResolver resolver; + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.resolver = resolver; + } + + @Override + public Map scan() { + Map> operationDataGroupedByChannelName = this.getOperationData().stream() + .filter(this::allFieldsAreNonNull) + .collect(groupingBy(OperationData::getChannelName)); + + return operationDataGroupedByChannelName.entrySet().stream() + .collect(toMap(Map.Entry::getKey, entry -> buildChannel(entry.getValue()))); + } + + private boolean allFieldsAreNonNull(OperationData operationData) { + boolean allNonNull = operationData.getChannelName() != null + && operationData.getPayloadType() != null + && operationData.getOperationBinding() != null; + + if (!allNonNull) { + log.warn("Some data fields are null - this method will not be documented: {}", operationData); + } + + return allNonNull; + } + + /** + * Creates an asyncapi {@link ChannelItem} using the given list of {@link OperationData}. Expects, that all {@link OperationData} + * items belong to the same channel. Most properties of the resulting {@link ChannelItem} are extracted from the + * first {@link OperationData} item in the list, assuming that all {@link OperationData} contains the same channel + * informations. + * + * @param operationDataList List of all {@link OperationData} items for a single channel. + * @return the resulting {@link ChannelItem} + */ + private ChannelItem buildChannel(List operationDataList) { + // All bindings in the group are assumed to be the same + // AsyncApi does not support multiple bindings on a single channel + Map channelBinding = + operationDataList.get(0).getChannelBinding(); + Map operationBinding = + operationDataList.get(0).getOperationBinding(); + Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; + Map chBinding = channelBinding != null ? new HashMap<>(channelBinding) : null; + String operationId = operationDataList.get(0).getChannelName() + "_" + + this.asyncAnnotationProvider.getOperationType().operationName; + String description = operationDataList.get(0).getDescription(); + List servers = operationDataList.get(0).getServers(); + + if (description.isEmpty()) { + description = "Auto-generated description"; + } + + Operation operation = Operation.builder() + .description(description) + .operationId(operationId) + .message(getMessageObject(operationDataList)) + .bindings(opBinding) + .build(); + + ChannelItem.ChannelItemBuilder channelBuilder = ChannelItem.builder().bindings(chBinding); + channelBuilder = switch (this.asyncAnnotationProvider.getOperationType()) { + case PUBLISH -> channelBuilder.publish(operation); + case SUBSCRIBE -> channelBuilder.subscribe(operation);}; + + // Only set servers if servers are defined. Avoid setting an emtpy list + // because this would generate empty server entries for each channel in the resulting + // async api. + if (servers != null && !servers.isEmpty()) { + validateServers(servers, operationId); + channelBuilder.servers(servers); + } + return channelBuilder.build(); + } + + private Object getMessageObject(List operationDataList) { + Set messages = + operationDataList.stream().map(this::buildMessage).collect(toSet()); + + return toMessageObjectOrComposition(messages); + } + + private Message buildMessage(OperationData operationData) { + Class payloadType = operationData.getPayloadType(); + String modelName = this.schemasService.register(payloadType); + String headerModelName = this.schemasService.register(operationData.getHeaders()); + + /* + * 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; + + var builder = Message.builder() + .name(payloadType.getName()) + .title(payloadType.getSimpleName()) + .description(description) + .payload(PayloadReference.fromModelName(modelName)) + .headers(HeaderReference.fromModelName(headerModelName)) + .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 (StringUtils.hasText(annotationMessageDescription)) { + builder.description(annotationMessageDescription); + } + + var name = annotationMessage.getName(); + if (StringUtils.hasText(name)) { + builder.name(name); + } + + var title = annotationMessage.getTitle(); + if (StringUtils.hasText(title)) { + builder.title(title); + } + } + } + + protected List getOperationData() { + return classScanner.scan().stream() + .flatMap(this::getAnnotatedMethods) + .flatMap(this::toOperationData) + .toList(); + } + + private Stream getAnnotatedMethods(Class type) { + Class annotationClass = this.asyncAnnotationProvider.getAnnotation(); + log.debug("Scanning class \"{}\" for @\"{}\" annotated methods", type.getName(), annotationClass.getName()); + + return Arrays.stream(type.getDeclaredMethods()) + .filter(method -> !method.isBridge()) + .filter(method -> AnnotationUtil.findAnnotation(annotationClass, method) != null); + } + + private Stream toOperationData(Method method) { + Class annotationClass = this.asyncAnnotationProvider.getAnnotation(); + log.debug("Mapping method \"{}\" to channels", method.getName()); + + Map operationBindings = + AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors); + Map messageBindings = + AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors); + Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method); + + Set annotations = AnnotationUtil.findAnnotations(annotationClass, method); + return annotations.stream() + .map(annotation -> toOperationData(method, operationBindings, messageBindings, message, annotation)); + } + + private OperationData toOperationData( + Method method, + Map operationBindings, + Map messageBindings, + Message message, + A annotation) { + AsyncOperation op = this.asyncAnnotationProvider.getAsyncOperation(annotation); + Class payloadType = + op.payloadType() != Object.class ? op.payloadType() : payloadClassExtractor.extractFrom(method); + return ConsumerData.builder() // temporarily reuse this data type + .channelName(resolver.resolveStringValue(op.channelName())) + .description(resolver.resolveStringValue(op.description())) + .servers(AsyncAnnotationScannerUtil.getServers(op, resolver)) + .headers(AsyncAnnotationScannerUtil.getAsyncHeaders(op, resolver)) + .payloadType(payloadType) + .operationBinding(operationBindings) + .messageBinding(messageBindings) + .message(message) + .build(); + } + + /** + * validates the given list of server names (for a specific operation) with the servers defined in the 'servers' part of + * the current AsyncApi. + * + * @param serversFromOperation the server names defined for the current operation + * @param operationId operationId of the current operation - used for exception messages + * @throws IllegalArgumentException if server from operation is not present in AsyncApi's servers definition. + */ + void validateServers(List serversFromOperation, String operationId) { + if (!serversFromOperation.isEmpty()) { + Map asyncApiServers = + this.asyncApiDocketService.getAsyncApiDocket().getServers(); + if (asyncApiServers == null || asyncApiServers.isEmpty()) { + throw new IllegalArgumentException(String.format( + "Operation '%s' defines server refs (%s) but there are no servers defined in this AsyncAPI.", + operationId, serversFromOperation)); + } + for (String server : serversFromOperation) { + if (!asyncApiServers.containsKey(server)) { + throw new IllegalArgumentException(String.format( + "Operation '%s' defines unknown server ref '%s'. This AsyncApi defines these server(s): %s", + operationId, server, asyncApiServers.keySet())); + } + } + } + } + + public interface AsyncAnnotationProvider { + Class getAnnotation(); + + AsyncOperation getAsyncOperation(A annotation); + + OperationData.OperationType getOperationType(); + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtil.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtil.java similarity index 94% rename from springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtil.java rename to springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtil.java index 82a4da2b9..285b52a23 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtil.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtil.java @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation; +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.operation.OperationBinding; @@ -7,6 +7,9 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; 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; diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListener.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListener.java index 3435cf7cc..22818a537 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListener.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListener.java @@ -30,6 +30,8 @@ * @KafkaAsyncOperationBinding * public void receiveMessage(MonetaryAmount payload) { ... } * + * + * Maintainer node: move to io.github.stavshamir.springwolf.asyncapi.annotation */ @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE}) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java deleted file mode 100644 index 4fcda78f3..000000000 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation; - -import com.asyncapi.v2.binding.message.MessageBinding; -import com.asyncapi.v2.binding.operation.OperationBinding; -import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.MessageBindingProcessor; -import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AnnotationUtil; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.AbstractOperationDataScanner; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; -import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; -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.configuration.AsyncApiDocketService; -import io.github.stavshamir.springwolf.schemas.SchemasService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.EmbeddedValueResolverAware; -import org.springframework.util.StringValueResolver; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -@Slf4j -@RequiredArgsConstructor -public class AsyncListenerAnnotationScanner extends AbstractOperationDataScanner implements EmbeddedValueResolverAware { - private StringValueResolver resolver; - private final ClassScanner classScanner; - private final SchemasService schemasService; - private final AsyncApiDocketService asyncApiDocketService; - - private final PayloadClassExtractor payloadClassExtractor; - - private final List operationBindingProcessors; - private final List messageBindingProcessors; - - @Override - public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.resolver = resolver; - } - - @Override - protected SchemasService getSchemaService() { - return this.schemasService; - } - - @Override - protected AsyncApiDocketService getAsyncApiDocketService() { - return asyncApiDocketService; - } - - @Override - protected List getOperationData() { - return classScanner.scan().stream() - .flatMap(this::getAnnotatedMethods) - .flatMap(this::toOperationData) - .toList(); - } - - private Stream getAnnotatedMethods(Class type) { - Class annotationClass = AsyncListener.class; - log.debug("Scanning class \"{}\" for @\"{}\" annotated methods", type.getName(), annotationClass.getName()); - - return Arrays.stream(type.getDeclaredMethods()) - .filter(method -> !method.isBridge()) - .filter(method -> AnnotationUtil.findAnnotation(annotationClass, method) != null); - } - - private Stream toOperationData(Method method) { - log.debug("Mapping method \"{}\" to channels", method.getName()); - - Map operationBindings = - AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors); - Map messageBindings = - AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors); - Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method); - - Set annotations = AnnotationUtil.findAnnotations(AsyncListener.class, method); - return annotations.stream() - .map(annotation -> toConsumerData(method, operationBindings, messageBindings, message, annotation)); - } - - private ConsumerData toConsumerData( - Method method, - Map operationBindings, - Map messageBindings, - Message message, - AsyncListener annotation) { - AsyncOperation op = annotation.operation(); - Class payloadType = - op.payloadType() != Object.class ? op.payloadType() : payloadClassExtractor.extractFrom(method); - return ConsumerData.builder() - .channelName(resolver.resolveStringValue(op.channelName())) - .description(resolver.resolveStringValue(op.description())) - .servers(AsyncAnnotationScannerUtil.getServers(op, resolver)) - .headers(AsyncAnnotationScannerUtil.getAsyncHeaders(op, resolver)) - .payloadType(payloadType) - .operationBinding(operationBindings) - .messageBinding(messageBindings) - .message(message) - .build(); - } - - @Override - protected OperationData.OperationType getOperationType() { - return OperationData.OperationType.PUBLISH; - } -} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java deleted file mode 100644 index 928552154..000000000 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation; - -import com.asyncapi.v2.binding.message.MessageBinding; -import com.asyncapi.v2.binding.operation.OperationBinding; -import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.MessageBindingProcessor; -import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AnnotationUtil; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.AbstractOperationDataScanner; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; -import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; -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.configuration.AsyncApiDocketService; -import io.github.stavshamir.springwolf.schemas.SchemasService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.EmbeddedValueResolverAware; -import org.springframework.util.StringValueResolver; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -@Slf4j -@RequiredArgsConstructor -public class AsyncPublisherAnnotationScanner extends AbstractOperationDataScanner - implements EmbeddedValueResolverAware { - private StringValueResolver resolver; - private final ClassScanner classScanner; - private final SchemasService schemasService; - private final AsyncApiDocketService asyncApiDocketService; - - private final PayloadClassExtractor payloadClassExtractor; - - private final List operationBindingProcessors; - private final List messageBindingProcessors; - - @Override - public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.resolver = resolver; - } - - @Override - protected SchemasService getSchemaService() { - return this.schemasService; - } - - @Override - protected AsyncApiDocketService getAsyncApiDocketService() { - return asyncApiDocketService; - } - - @Override - protected List getOperationData() { - return classScanner.scan().stream() - .flatMap(this::getAnnotatedMethods) - .flatMap(this::toOperationData) - .toList(); - } - - private Stream getAnnotatedMethods(Class type) { - Class annotationClass = AsyncPublisher.class; - log.debug("Scanning class \"{}\" for @\"{}\" annotated methods", type.getName(), annotationClass.getName()); - - return Arrays.stream(type.getDeclaredMethods()) - .filter(method -> !method.isBridge()) - .filter(method -> AnnotationUtil.findAnnotation(annotationClass, method) != null); - } - - private Stream toOperationData(Method method) { - log.debug("Mapping method \"{}\" to channels", method.getName()); - - Map operationBindings = - AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors); - Map messageBindings = - AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors); - Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method); - - Set annotations = AnnotationUtil.findAnnotations(AsyncPublisher.class, method); - return annotations.stream() - .map(annotation -> toConsumerData(method, operationBindings, messageBindings, message, annotation)); - } - - private ProducerData toConsumerData( - Method method, - Map operationBindings, - Map messageBindings, - Message message, - AsyncPublisher annotation) { - AsyncOperation op = annotation.operation(); - Class payloadType = - op.payloadType() != Object.class ? op.payloadType() : payloadClassExtractor.extractFrom(method); - return ProducerData.builder() - .channelName(resolver.resolveStringValue(op.channelName())) - .description(resolver.resolveStringValue(op.description())) - .servers(AsyncAnnotationScannerUtil.getServers(op, resolver)) - .headers(AsyncAnnotationScannerUtil.getAsyncHeaders(op, resolver)) - .payloadType(payloadType) - .operationBinding(operationBindings) - .messageBinding(messageBindings) - .message(message) - .build(); - } - - @Override - protected OperationData.OperationType getOperationType() { - return OperationData.OperationType.SUBSCRIBE; - } -} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerIntegrationTest.java similarity index 84% rename from springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java rename to springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerIntegrationTest.java index b81b1fdd1..140385627 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerIntegrationTest.java @@ -1,13 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation; +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; import com.asyncapi.v2._6_0.model.channel.ChannelItem; import com.asyncapi.v2._6_0.model.channel.operation.Operation; import com.asyncapi.v2._6_0.model.info.Info; import com.asyncapi.v2._6_0.model.server.Server; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.MessageBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.TestOperationBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncMessage; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; +import io.github.stavshamir.springwolf.asyncapi.types.OperationData; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; @@ -16,6 +22,7 @@ import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; +import io.github.stavshamir.springwolf.schemas.SchemasService; import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -23,14 +30,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.StringValueResolver; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -42,43 +44,53 @@ import java.util.Set; import static java.util.Collections.EMPTY_MAP; +import static java.util.Collections.emptyList; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@ExtendWith(SpringExtension.class) -@ContextConfiguration( - classes = { - AsyncListenerAnnotationScanner.class, - DefaultSchemasService.class, - PayloadClassExtractor.class, - ExampleJsonGenerator.class, - SpringwolfConfigProperties.class, - TestOperationBindingProcessor.class - }) -@TestPropertySource( - properties = { - "test.property.test-channel=test-channel", - "test.property.description=description", - "test.property.server1=server1", - "test.property.server2=server2" - }) -class AsyncListenerAnnotationScannerIntegrationTest { - - @Autowired - private AsyncListenerAnnotationScanner channelScanner; - - @MockBean - private ClassScanner classScanner; - - @MockBean - private AsyncApiDocketService asyncApiDocketService; +class AsyncAnnotationScannerIntegrationTest { - private void setClassToScan(Class classToScan) { - Set> classesToScan = singleton(classToScan); - when(classScanner.scan()).thenReturn(classesToScan); - } + private AsyncAnnotationChannelsScanner.AsyncAnnotationProvider asyncAnnotationProvider = new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { + @Override + public Class getAnnotation() { + return AsyncListener.class; + } + + @Override + public AsyncOperation getAsyncOperation(AsyncListener annotation) { + return annotation.operation(); + } + + @Override + public OperationData.OperationType getOperationType() { + return OperationData.OperationType.PUBLISH; + } + }; + private final SpringwolfConfigProperties properties = new SpringwolfConfigProperties(); + private final ClassScanner classScanner = mock(ClassScanner.class); + private final SchemasService schemasService = + new DefaultSchemasService(emptyList(), new ExampleJsonGenerator(), properties); + private final AsyncApiDocketService asyncApiDocketService = mock(AsyncApiDocketService.class); + private final PayloadClassExtractor payloadClassExtractor = new PayloadClassExtractor(properties); + + private final List operationBindingProcessors = + List.of(new TestOperationBindingProcessor()); + private final List messageBindingProcessors = emptyList(); + + private final StringValueResolver stringValueResolver = mock(StringValueResolver.class); + + private final AsyncAnnotationChannelsScanner channelScanner = new AsyncAnnotationChannelsScanner<>( + asyncAnnotationProvider, + classScanner, + schemasService, + asyncApiDocketService, + payloadClassExtractor, + operationBindingProcessors, + messageBindingProcessors); @BeforeEach public void setup() { @@ -88,6 +100,21 @@ public void setup() { .server("server1", new Server()) .server("server2", new Server()) .build()); + + channelScanner.setEmbeddedValueResolver(stringValueResolver); + when(stringValueResolver.resolveStringValue(any())) + .thenAnswer(invocation -> switch ((String) invocation.getArgument(0)) { + case "${test.property.test-channel}" -> "test-channel"; + case "${test.property.description}" -> "description"; + case "${test.property.server1}" -> "server1"; + case "${test.property.server2}" -> "server2"; + default -> invocation.getArgument(0); + }); + } + + private void setClassToScan(Class classToScan) { + Set> classesToScan = singleton(classToScan); + when(classScanner.scan()).thenReturn(classesToScan); } @Test diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtilTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtilTest.java similarity index 97% rename from springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtilTest.java rename to springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtilTest.java index 539c264a7..224705a7c 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtilTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtilTest.java @@ -1,11 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation; +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.TestAbstractOperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.TestMessageBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.TestOperationBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncMessage; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import org.assertj.core.util.Maps; diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java deleted file mode 100644 index 272656219..000000000 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java +++ /dev/null @@ -1,397 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation; - -import com.asyncapi.v2._6_0.model.channel.ChannelItem; -import com.asyncapi.v2._6_0.model.channel.operation.Operation; -import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.TestOperationBindingProcessor; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; -import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference; -import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; -import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; -import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; -import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.util.Map; -import java.util.Set; - -import static java.util.Collections.EMPTY_MAP; -import static java.util.Collections.singleton; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@ExtendWith(SpringExtension.class) -@ContextConfiguration( - classes = { - AsyncPublisherAnnotationScanner.class, - DefaultSchemasService.class, - PayloadClassExtractor.class, - ExampleJsonGenerator.class, - SpringwolfConfigProperties.class, - TestOperationBindingProcessor.class - }) -@TestPropertySource(properties = {"test.property.test-channel=test-channel", "test.property.description=description"}) -class AsyncPublisherAnnotationScannerIntegrationTest { - - @Autowired - private AsyncPublisherAnnotationScanner channelScanner; - - @MockBean - private ClassScanner classScanner; - - @MockBean - private AsyncApiDocketService asyncApiDocketService; - - private void setClassToScan(Class classToScan) { - Set> classesToScan = singleton(classToScan); - when(classScanner.scan()).thenReturn(classesToScan); - } - - @Test - void scan_componentHasNoPublisherMethods() { - setClassToScan(ClassWithoutPublisherAnnotation.class); - - Map channels = channelScanner.scan(); - - assertThat(channels).isEmpty(); - } - - @Test - void scan_componentHasPublisherMethod() { - // Given a class with methods annotated with AsyncPublisher, where only the channel-name is set - setClassToScan(ClassWithPublisherAnnotation.class); - - // When scan is called - Map actualChannels = channelScanner.scan(); - - // Then the returned collection contains the channel - Message message = Message.builder() - .name(SimpleFoo.class.getName()) - .title(SimpleFoo.class.getSimpleName()) - .description(null) - .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) - .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) - .bindings(EMPTY_MAP) - .build(); - - Operation operation = Operation.builder() - .description("Auto-generated description") - .operationId("test-channel_subscribe") - .bindings(EMPTY_MAP) - .message(message) - .build(); - - ChannelItem expectedChannel = - ChannelItem.builder().bindings(null).subscribe(operation).build(); - - assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - } - - @Test - void scan_componentHasPublisherMethodWithAllAttributes() { - // Given a class with method annotated with AsyncPublisher, where all attributes are set - setClassToScan(ClassWithPublisherAnnotationWithAllAttributes.class); - - // When scan is called - Map actualChannels = channelScanner.scan(); - - // Then the returned collection contains the channel - Message message = Message.builder() - .name(SimpleFoo.class.getName()) - .title(SimpleFoo.class.getSimpleName()) - .description(null) - .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) - .headers(HeaderReference.fromModelName("TestSchema")) - .bindings(EMPTY_MAP) - .build(); - - Operation operation = Operation.builder() - .description("description") - .operationId("test-channel_subscribe") - .bindings(Map.of(TestOperationBindingProcessor.TYPE, TestOperationBindingProcessor.BINDING)) - .message(message) - .build(); - - ChannelItem expectedChannel = - ChannelItem.builder().bindings(null).subscribe(operation).build(); - - assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - } - - @Test - void scan_componentHasMultiplePublisherAnnotations() { - // Given a class with methods annotated with AsyncListener, where only the channel-name is set - setClassToScan(ClassWithMultipleListenerAnnotations.class); - - // When scan is called - Map actualChannels = channelScanner.scan(); - - // Then the returned collection contains the channel - Message message = Message.builder() - .name(SimpleFoo.class.getName()) - .title(SimpleFoo.class.getSimpleName()) - .description(null) - .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) - .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) - .bindings(EMPTY_MAP) - .build(); - - Operation operation1 = Operation.builder() - .description("Auto-generated description") - .operationId("test-channel-1_subscribe") - .bindings(EMPTY_MAP) - .message(message) - .build(); - - ChannelItem expectedChannel1 = - ChannelItem.builder().bindings(null).subscribe(operation1).build(); - - Operation operation2 = Operation.builder() - .description("Auto-generated description") - .operationId("test-channel-2_subscribe") - .bindings(EMPTY_MAP) - .message(message) - .build(); - - ChannelItem expectedChannel2 = - ChannelItem.builder().bindings(null).subscribe(operation2).build(); - - assertThat(actualChannels) - .containsExactlyInAnyOrderEntriesOf(Map.of( - "test-channel-1", expectedChannel1, - "test-channel-2", expectedChannel2)); - } - - @Test - void scan_componentHasPublisherMethodWithAsyncMessageAnnotation() { - // Given a class with methods annotated with AsyncListener, where only the channel-name is set - setClassToScan(ClassWithMessageAnnotation.class); - - // When scan is called - Map 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("Auto-generated description") - .operationId("test-channel_subscribe") - .bindings(EMPTY_MAP) - .message(message) - .build(); - - ChannelItem expectedChannel = - ChannelItem.builder().bindings(null).subscribe(operation).build(); - - assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - } - - private static class ClassWithoutPublisherAnnotation { - - private void methodWithoutAnnotation() {} - } - - private static class ClassWithPublisherAnnotation { - - @AsyncPublisher(operation = @AsyncOperation(channelName = "test-channel")) - private void methodWithAnnotation(SimpleFoo payload) {} - - private void methodWithoutAnnotation() {} - } - - private static class ClassWithPublisherAnnotationWithAllAttributes { - - @AsyncPublisher( - operation = - @AsyncOperation( - channelName = "${test.property.test-channel}", - description = "${test.property.description}", - headers = - @AsyncOperation.Headers( - schemaName = "TestSchema", - values = { - @AsyncOperation.Headers.Header(name = "header", value = "value") - }))) - @TestOperationBindingProcessor.TestOperationBinding() - private void methodWithAnnotation(SimpleFoo payload) {} - - private void methodWithoutAnnotation() {} - } - - private static class ClassWithMultipleListenerAnnotations { - - @AsyncPublisher(operation = @AsyncOperation(channelName = "test-channel-1")) - @AsyncPublisher(operation = @AsyncOperation(channelName = "test-channel-2")) - private void methodWithMultipleAnnotation(SimpleFoo payload) {} - } - - private static class ClassWithMessageAnnotation { - - @AsyncPublisher( - operation = - @AsyncOperation( - channelName = "test-channel", - message = - @AsyncMessage( - description = "Message description", - messageId = "simpleFoo", - name = "SimpleFooPayLoad", - schemaFormat = "application/schema+json;version=draft-07", - title = "Message Title"))) - private void methodWithAnnotation(SimpleFoo payload) {} - - private void methodWithoutAnnotation() {} - } - - @Nested - class ImplementingInterface { - @ParameterizedTest - @ValueSource(classes = {ClassImplementingInterface.class, ClassImplementingInterfaceWithAnnotation.class}) - void scan_componentHasOnlyDeclaredMethods(Class clazz) { - // Given a class with a method, which is declared in a generic interface - setClassToScan(clazz); - - // When scan is called - Map actualChannels = channelScanner.scan(); - - // Then the returned collection contains the channel with the actual method, excluding type erased methods - Message message = Message.builder() - .name(String.class.getName()) - .title(String.class.getSimpleName()) - .description(null) - .payload(PayloadReference.fromModelName(String.class.getSimpleName())) - .schemaFormat("application/vnd.oai.openapi+json;version=3.0.0") - .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) - .bindings(EMPTY_MAP) - .build(); - - Operation operation = Operation.builder() - .description("test channel operation description") - .operationId("test-channel_subscribe") - .bindings(EMPTY_MAP) - .message(message) - .build(); - - ChannelItem expectedChannel = - ChannelItem.builder().bindings(null).subscribe(operation).build(); - - assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - } - - private static class ClassImplementingInterface implements ClassInterface { - - @AsyncPublisher( - operation = - @AsyncOperation( - channelName = "test-channel", - description = "test channel operation description")) - @Override - public void methodFromInterface(String payload) {} - } - - interface ClassInterface { - void methodFromInterface(T payload); - } - - private static class ClassImplementingInterfaceWithAnnotation implements ClassInterfaceWithAnnotation { - - @Override - public void methodFromInterface(String payload) {} - } - - interface ClassInterfaceWithAnnotation { - @AsyncPublisher( - operation = - @AsyncOperation( - channelName = "test-channel", - description = "test channel operation description")) - void methodFromInterface(T payload); - } - } - - @Nested - class MetaAnnotation { - @Test - void scan_componentHasPublisherMethodWithMetaAnnotation() { - // Given a class with methods annotated with a AsyncPublisher meta annotation - setClassToScan(ClassWithMetaAnnotation.class); - - // When scan is called - Map actualChannels = channelScanner.scan(); - - // Then the returned collection contains the channel - Message message = Message.builder() - .name(String.class.getName()) - .title(String.class.getSimpleName()) - .description(null) - .payload(PayloadReference.fromModelName(String.class.getSimpleName())) - .schemaFormat("application/vnd.oai.openapi+json;version=3.0.0") - .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) - .bindings(EMPTY_MAP) - .build(); - - Operation operation = Operation.builder() - .description("test channel operation description") - .operationId("test-channel_subscribe") - .bindings(EMPTY_MAP) - .message(message) - .build(); - - ChannelItem expectedChannel = - ChannelItem.builder().bindings(null).subscribe(operation).build(); - - assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - } - - public static class ClassWithMetaAnnotation { - @AsyncPublisherMetaAnnotation - void methodFromInterface(String payload) {} - } - - @Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) - @Retention(RetentionPolicy.RUNTIME) - @Inherited - @AsyncPublisher( - operation = - @AsyncOperation( - channelName = "test-channel", - description = "test channel operation description")) - public @interface AsyncPublisherMetaAnnotation {} - } - - @Data - @NoArgsConstructor - private static class SimpleFoo { - private String s; - private boolean b; - } -} From 45bb8ab2a4da533e710002eca68a7cbfe45a3ccf Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sat, 16 Dec 2023 21:12:38 +0100 Subject: [PATCH 2/2] refactor(core): remove usage of ConsumerData in AsyncAnnotationChannelsScanner --- .../SpringwolfScannerConfiguration.java | 10 +- .../AsyncAnnotationChannelsScanner.java | 231 ++++++------------ .../AsyncAnnotationScannerUtil.java | 55 ++--- .../ClassLevelAnnotationChannelsScanner.java | 4 +- .../MethodLevelAnnotationChannelsScanner.java | 4 +- ...> AsyncAnnotationChannelsScannerTest.java} | 54 ++-- .../AsyncAnnotationScannerUtilTest.java | 22 +- 7 files changed, 154 insertions(+), 226 deletions(-) rename springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/{AsyncAnnotationScannerIntegrationTest.java => AsyncAnnotationChannelsScannerTest.java} (94%) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java index e3ad1e305..100f86cc0 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfScannerConfiguration.java @@ -6,9 +6,9 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.MessageBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncAnnotationChannelsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ConsumerOperationDataScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProducerOperationDataScanner; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncAnnotationChannelsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; @@ -95,7 +95,7 @@ public AsyncAnnotationChannelsScanner asyncListenerAnnotationScan List operationBindingProcessors, List messageBindingProcessors) { return new AsyncAnnotationChannelsScanner<>( - buidAsyncListenerAnnotationProvider(), + buildAsyncListenerAnnotationProvider(), springwolfClassScanner, schemasService, asyncApiDocketService, @@ -127,7 +127,8 @@ public AsyncAnnotationChannelsScanner asyncPublisherAnnotationSc messageBindingProcessors); } - private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider buidAsyncListenerAnnotationProvider() { + private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider + buildAsyncListenerAnnotationProvider() { return new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { @Override public Class getAnnotation() { @@ -146,7 +147,8 @@ public OperationData.OperationType getOperationType() { }; } - private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider buildAsyncPublisherAnnotationProvider() { + private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider + buildAsyncPublisherAnnotationProvider() { return new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { @Override public Class getAnnotation() { diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java index bd11e8e8a..805af5050 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScanner.java @@ -4,19 +4,19 @@ import com.asyncapi.v2._6_0.model.channel.ChannelItem; import com.asyncapi.v2._6_0.model.channel.operation.Operation; import com.asyncapi.v2._6_0.model.server.Server; -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.scanners.bindings.MessageBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelMerger; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; -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.asyncapi.types.channel.operation.message.PayloadReference; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference; import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -33,13 +33,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Stream; -import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessageObjectOrComposition; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toMap; -import static java.util.stream.Collectors.toSet; +import static java.util.stream.Collectors.toList; @Slf4j @RequiredArgsConstructor @@ -62,189 +58,120 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) { @Override public Map scan() { - Map> operationDataGroupedByChannelName = this.getOperationData().stream() - .filter(this::allFieldsAreNonNull) - .collect(groupingBy(OperationData::getChannelName)); + List> channels = classScanner.scan().stream() + .flatMap(this::getAnnotatedMethods) + .map(this::buildChannelItem) + .filter(this::isInvalidChannelItem) + .collect(toList()); - return operationDataGroupedByChannelName.entrySet().stream() - .collect(toMap(Map.Entry::getKey, entry -> buildChannel(entry.getValue()))); + return ChannelMerger.merge(channels); } - private boolean allFieldsAreNonNull(OperationData operationData) { - boolean allNonNull = operationData.getChannelName() != null - && operationData.getPayloadType() != null - && operationData.getOperationBinding() != null; + private Stream> getAnnotatedMethods(Class type) { + Class annotationClass = this.asyncAnnotationProvider.getAnnotation(); + log.debug("Scanning class \"{}\" for @\"{}\" annotated methods", type.getName(), annotationClass.getName()); + + return Arrays.stream(type.getDeclaredMethods()) + .filter(method -> !method.isBridge()) + .filter(method -> AnnotationUtil.findAnnotation(annotationClass, method) != null) + .peek(method -> log.debug("Mapping method \"{}\" to channels", method.getName())) + .flatMap(method -> AnnotationUtil.findAnnotations(annotationClass, method).stream() + .map(annotation -> new MethodAndAnnotation<>(method, annotation))); + } + + private boolean isInvalidChannelItem(Map.Entry entry) { + Operation publish = entry.getValue().getPublish(); + boolean publishBindingExists = publish != null && publish.getBindings() != null; + + Operation subscribe = entry.getValue().getSubscribe(); + boolean subscribeBindingExists = subscribe != null && subscribe.getBindings() != null; + + boolean allNonNull = entry.getKey() != null && (publishBindingExists || subscribeBindingExists); if (!allNonNull) { - log.warn("Some data fields are null - this method will not be documented: {}", operationData); + log.warn( + "Some data fields are null - method (channel={}) will not be documented: {}", + entry.getKey(), + entry.getValue()); } return allNonNull; } - /** - * Creates an asyncapi {@link ChannelItem} using the given list of {@link OperationData}. Expects, that all {@link OperationData} - * items belong to the same channel. Most properties of the resulting {@link ChannelItem} are extracted from the - * first {@link OperationData} item in the list, assuming that all {@link OperationData} contains the same channel - * informations. - * - * @param operationDataList List of all {@link OperationData} items for a single channel. - * @return the resulting {@link ChannelItem} - */ - private ChannelItem buildChannel(List operationDataList) { - // All bindings in the group are assumed to be the same - // AsyncApi does not support multiple bindings on a single channel - Map channelBinding = - operationDataList.get(0).getChannelBinding(); - Map operationBinding = - operationDataList.get(0).getOperationBinding(); - Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; - Map chBinding = channelBinding != null ? new HashMap<>(channelBinding) : null; - String operationId = operationDataList.get(0).getChannelName() + "_" - + this.asyncAnnotationProvider.getOperationType().operationName; - String description = operationDataList.get(0).getDescription(); - List servers = operationDataList.get(0).getServers(); - - if (description.isEmpty()) { - description = "Auto-generated description"; - } + private Map.Entry buildChannelItem(MethodAndAnnotation methodAndAnnotation) { + ChannelItem.ChannelItemBuilder channelBuilder = ChannelItem.builder(); - Operation operation = Operation.builder() - .description(description) - .operationId(operationId) - .message(getMessageObject(operationDataList)) - .bindings(opBinding) - .build(); + AsyncOperation operationAnnotation = + this.asyncAnnotationProvider.getAsyncOperation(methodAndAnnotation.annotation()); + String channelName = resolver.resolveStringValue(operationAnnotation.channelName()); - ChannelItem.ChannelItemBuilder channelBuilder = ChannelItem.builder().bindings(chBinding); - channelBuilder = switch (this.asyncAnnotationProvider.getOperationType()) { + Operation operation = buildOperation(operationAnnotation, methodAndAnnotation.method(), channelName); + switch (this.asyncAnnotationProvider.getOperationType()) { case PUBLISH -> channelBuilder.publish(operation); - case SUBSCRIBE -> channelBuilder.subscribe(operation);}; + case SUBSCRIBE -> channelBuilder.subscribe(operation); + } + ; - // Only set servers if servers are defined. Avoid setting an emtpy list - // because this would generate empty server entries for each channel in the resulting - // async api. + List servers = AsyncAnnotationScannerUtil.getServers(operationAnnotation, resolver); if (servers != null && !servers.isEmpty()) { - validateServers(servers, operationId); + validateServers(servers, operation.getOperationId()); channelBuilder.servers(servers); } - return channelBuilder.build(); + + ChannelItem channelItem = channelBuilder.build(); + return Map.entry(channelName, channelItem); } - private Object getMessageObject(List operationDataList) { - Set messages = - operationDataList.stream().map(this::buildMessage).collect(toSet()); + private Operation buildOperation(AsyncOperation asyncOperation, Method method, String channelName) { + String description = this.resolver.resolveStringValue(asyncOperation.description()); + if (!StringUtils.hasText(description)) { + description = "Auto-generated description"; + } + + String operationId = channelName + "_" + this.asyncAnnotationProvider.getOperationType().operationName; - return toMessageObjectOrComposition(messages); + Map operationBinding = + AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors); + Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; + + return Operation.builder() + .description(description) + .operationId(operationId) + .message(buildMessage(asyncOperation, method)) + .bindings(opBinding) + .build(); } - private Message buildMessage(OperationData operationData) { - Class payloadType = operationData.getPayloadType(); - String modelName = this.schemasService.register(payloadType); - String headerModelName = this.schemasService.register(operationData.getHeaders()); + private Message buildMessage(AsyncOperation operationData, Method method) { + Class payloadType = operationData.payloadType() != Object.class + ? operationData.payloadType() + : payloadClassExtractor.extractFrom(method); - /* - * 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. - */ + String modelName = this.schemasService.register(payloadType); + AsyncHeaders asyncHeaders = AsyncAnnotationScannerUtil.getAsyncHeaders(operationData, resolver); + String headerModelName = this.schemasService.register(asyncHeaders); var schema = payloadType.getAnnotation(Schema.class); String description = schema != null ? schema.description() : null; + Map messageBinding = + AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors); + var builder = Message.builder() .name(payloadType.getName()) .title(payloadType.getSimpleName()) .description(description) .payload(PayloadReference.fromModelName(modelName)) .headers(HeaderReference.fromModelName(headerModelName)) - .bindings(operationData.getMessageBinding()); + .bindings(messageBinding); // 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); + // priority so if we find them, we need to override the default values. + AsyncAnnotationScannerUtil.processAsyncMessageAnnotation(builder, operationData.message(), this.resolver); 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 (StringUtils.hasText(annotationMessageDescription)) { - builder.description(annotationMessageDescription); - } - - var name = annotationMessage.getName(); - if (StringUtils.hasText(name)) { - builder.name(name); - } - - var title = annotationMessage.getTitle(); - if (StringUtils.hasText(title)) { - builder.title(title); - } - } - } - - protected List getOperationData() { - return classScanner.scan().stream() - .flatMap(this::getAnnotatedMethods) - .flatMap(this::toOperationData) - .toList(); - } - - private Stream getAnnotatedMethods(Class type) { - Class annotationClass = this.asyncAnnotationProvider.getAnnotation(); - log.debug("Scanning class \"{}\" for @\"{}\" annotated methods", type.getName(), annotationClass.getName()); - - return Arrays.stream(type.getDeclaredMethods()) - .filter(method -> !method.isBridge()) - .filter(method -> AnnotationUtil.findAnnotation(annotationClass, method) != null); - } - - private Stream toOperationData(Method method) { - Class annotationClass = this.asyncAnnotationProvider.getAnnotation(); - log.debug("Mapping method \"{}\" to channels", method.getName()); - - Map operationBindings = - AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors); - Map messageBindings = - AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors); - Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method); - - Set annotations = AnnotationUtil.findAnnotations(annotationClass, method); - return annotations.stream() - .map(annotation -> toOperationData(method, operationBindings, messageBindings, message, annotation)); - } - - private OperationData toOperationData( - Method method, - Map operationBindings, - Map messageBindings, - Message message, - A annotation) { - AsyncOperation op = this.asyncAnnotationProvider.getAsyncOperation(annotation); - Class payloadType = - op.payloadType() != Object.class ? op.payloadType() : payloadClassExtractor.extractFrom(method); - return ConsumerData.builder() // temporarily reuse this data type - .channelName(resolver.resolveStringValue(op.channelName())) - .description(resolver.resolveStringValue(op.description())) - .servers(AsyncAnnotationScannerUtil.getServers(op, resolver)) - .headers(AsyncAnnotationScannerUtil.getAsyncHeaders(op, resolver)) - .payloadType(payloadType) - .operationBinding(operationBindings) - .messageBinding(messageBindings) - .message(message) - .build(); - } - /** * validates the given list of server names (for a specific operation) with the servers defined in the 'servers' part of * the current AsyncApi. @@ -279,4 +206,6 @@ public interface AsyncAnnotationProvider { OperationData.OperationType getOperationType(); } + + private record MethodAndAnnotation(Method method, A annotation) {} } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtil.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtil.java index 285b52a23..9c0100f37 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtil.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtil.java @@ -7,17 +7,14 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncMessage; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; 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.lang.NonNull; 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; @@ -89,42 +86,31 @@ public static Map processMessageBindingFromAnnotation( ProcessedMessageBinding::getType, ProcessedMessageBinding::getBinding, (e1, e2) -> e2)); } - public static Message processMessageFromAnnotation(Method method) { - var messageBuilder = Message.builder(); + public static void processAsyncMessageAnnotation( + Message.MessageBuilder messageBuilder, AsyncMessage asyncMessage, StringValueResolver resolver) { + String annotationMessageDescription = resolver.resolveStringValue(asyncMessage.description()); + if (StringUtils.hasText(annotationMessageDescription)) { + messageBuilder.description(annotationMessageDescription); + } - Annotation[] annotations = method.getAnnotations(); - for (Annotation annotation : annotations) { - if (annotation instanceof AsyncListener asyncListener) { - return parseMessage(asyncListener.operation()); - } else if (annotation instanceof AsyncPublisher asyncPublisher) { - return parseMessage(asyncPublisher.operation()); - } + String annotationMessageId = resolver.resolveStringValue(asyncMessage.messageId()); + if (StringUtils.hasText(annotationMessageId)) { + messageBuilder.messageId(annotationMessageId); } - return messageBuilder.build(); - } + String annotationName = resolver.resolveStringValue(asyncMessage.name()); + if (StringUtils.hasText(annotationName)) { + messageBuilder.name(annotationName); + } - private static Message parseMessage(AsyncOperation asyncOperation) { - var messageBuilder = Message.builder(); + String annotationSchemaFormat = asyncMessage.schemaFormat(); + var schemaFormat = annotationSchemaFormat != null ? annotationSchemaFormat : Message.DEFAULT_SCHEMA_FORMAT; + messageBuilder.schemaFormat(schemaFormat); - var asyncMessage = asyncOperation.message(); - if (StringUtils.hasText(asyncMessage.description())) { - messageBuilder.description(asyncMessage.description()); - } - if (StringUtils.hasText(asyncMessage.messageId())) { - messageBuilder.messageId(asyncMessage.messageId()); + String annotationTitle = resolver.resolveStringValue(asyncMessage.title()); + if (StringUtils.hasText(annotationTitle)) { + messageBuilder.title(annotationTitle); } - if (StringUtils.hasText(asyncMessage.name())) { - messageBuilder.name(asyncMessage.name()); - } - if (StringUtils.hasText(asyncMessage.schemaFormat())) { - messageBuilder.schemaFormat(asyncMessage.schemaFormat()); - } - if (StringUtils.hasText(asyncMessage.title())) { - messageBuilder.title(asyncMessage.title()); - } - - return messageBuilder.build(); } /** @@ -135,7 +121,6 @@ private static Message parseMessage(AsyncOperation asyncOperation) { * @param resolver the StringValueResolver to resolve placeholders * @return List of server names */ - @NonNull public static List getServers(AsyncOperation op, StringValueResolver resolver) { return Arrays.stream(op.servers()).map(resolver::resolveStringValue).collect(Collectors.toList()); } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScanner.java index 3d4599f5d..77c103811 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScanner.java @@ -79,12 +79,12 @@ private Stream> mapClassToChannel(Class compon String channelName = bindingFactory.getChannelName(classAnnotation); String operationId = channelName + "_publish_" + component.getSimpleName(); - ChannelItem channelItem = buildChannel(classAnnotation, operationId, annotatedMethods); + ChannelItem channelItem = buildChannelItem(classAnnotation, operationId, annotatedMethods); return Stream.of(Map.entry(channelName, channelItem)); } - private ChannelItem buildChannel(ClassAnnotation classAnnotation, String operationId, Set methods) { + private ChannelItem buildChannelItem(ClassAnnotation classAnnotation, String operationId, Set methods) { Object message = buildMessageObject(classAnnotation, methods); Operation operation = buildOperation(classAnnotation, operationId, message); return buildChannelItem(classAnnotation, operation); diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScanner.java index 2ccfa558b..f52d02290 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScanner.java @@ -56,12 +56,12 @@ private Map.Entry mapMethodToChannel(Method method) { String operationId = channelName + "_publish_" + method.getName(); Class payload = payloadClassExtractor.extractFrom(method); - ChannelItem channelItem = buildChannel(annotation, operationId, payload); + ChannelItem channelItem = buildChannelItem(annotation, operationId, payload); return Map.entry(channelName, channelItem); } - private ChannelItem buildChannel(MethodAnnotation annotation, String operationId, Class payloadType) { + private ChannelItem buildChannelItem(MethodAnnotation annotation, String operationId, Class payloadType) { Message message = buildMessage(annotation, payloadType); Operation operation = buildOperation(annotation, operationId, message); return buildChannelItem(annotation, operation); diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScannerTest.java similarity index 94% rename from springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerIntegrationTest.java rename to springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScannerTest.java index 140385627..1330c0e3f 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScannerTest.java @@ -52,24 +52,25 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -class AsyncAnnotationScannerIntegrationTest { - - private AsyncAnnotationChannelsScanner.AsyncAnnotationProvider asyncAnnotationProvider = new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { - @Override - public Class getAnnotation() { - return AsyncListener.class; - } - - @Override - public AsyncOperation getAsyncOperation(AsyncListener annotation) { - return annotation.operation(); - } - - @Override - public OperationData.OperationType getOperationType() { - return OperationData.OperationType.PUBLISH; - } - }; +class AsyncAnnotationChannelsScannerTest { + + private AsyncAnnotationChannelsScanner.AsyncAnnotationProvider asyncAnnotationProvider = + new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { + @Override + public Class getAnnotation() { + return AsyncListener.class; + } + + @Override + public AsyncOperation getAsyncOperation(AsyncListener annotation) { + return annotation.operation(); + } + + @Override + public OperationData.OperationType getOperationType() { + return OperationData.OperationType.PUBLISH; + } + }; private final SpringwolfConfigProperties properties = new SpringwolfConfigProperties(); private final ClassScanner classScanner = mock(ClassScanner.class); private final SchemasService schemasService = @@ -146,7 +147,7 @@ void scan_componentHasListenerMethod() { .build(); Operation operation = Operation.builder() - .description("test channel operation description") + .description("Auto-generated description") .operationId("test-channel_publish") .bindings(EMPTY_MAP) .message(message) @@ -179,11 +180,11 @@ void scan_componentHasListenerMethodWithAllAttributes() { // Then the returned collection contains the channel Message message = Message.builder() - .name(SimpleFoo.class.getName()) - .title(SimpleFoo.class.getSimpleName()) - .description("SimpleFoo Message Description") + .name(String.class.getName()) + .title(String.class.getSimpleName()) + .description(null) .schemaFormat(Message.DEFAULT_SCHEMA_FORMAT) - .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) + .payload(PayloadReference.fromModelName(String.class.getSimpleName())) .headers(HeaderReference.fromModelName("TestSchema")) .bindings(EMPTY_MAP) .build(); @@ -287,11 +288,7 @@ private void methodWithoutAnnotation() {} private static class ClassWithListenerAnnotation { - @AsyncListener( - operation = - @AsyncOperation( - channelName = "test-channel", - description = "test channel operation description")) + @AsyncListener(operation = @AsyncOperation(channelName = "test-channel")) private void methodWithAnnotation(SimpleFoo payload) {} private void methodWithoutAnnotation() {} @@ -315,6 +312,7 @@ private static class ClassWithListenerAnnotationWithAllAttributes { @AsyncOperation( channelName = "${test.property.test-channel}", description = "${test.property.description}", + payloadType = String.class, servers = {"${test.property.server1}", "${test.property.server2}"}, headers = @AsyncOperation.Headers( diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtilTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtilTest.java index 224705a7c..88b4d77e7 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtilTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScannerUtilTest.java @@ -119,13 +119,20 @@ void processMessageFromAnnotationWithoutAsyncMessage(Class classWithOperation throws NoSuchMethodException { // given Method method = classWithOperationBindingProcessor.getDeclaredMethod("methodWithAnnotation", String.class); + AsyncMessage message = + method.getAnnotation(AsyncListener.class).operation().message(); + + StringValueResolver stringResolver = mock(StringValueResolver.class); + when(stringResolver.resolveStringValue(any())) + .thenAnswer(invocation -> invocation.getArgument(0).toString()); // when - Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method); + Message.MessageBuilder actual = Message.builder(); + AsyncAnnotationScannerUtil.processAsyncMessageAnnotation(actual, message, stringResolver); // then var expectedMessage = Message.builder().build(); - assertEquals(expectedMessage, message); + assertEquals(expectedMessage, actual.build()); } @ParameterizedTest @@ -135,9 +142,16 @@ void processMessageFromAnnotationWithAsyncMessage(Class classWithOperationBin // given Method method = classWithOperationBindingProcessor.getDeclaredMethod("methodWithAsyncMessageAnnotation", String.class); + AsyncMessage message = + method.getAnnotation(AsyncListener.class).operation().message(); + + StringValueResolver stringResolver = mock(StringValueResolver.class); + when(stringResolver.resolveStringValue(any())) + .thenAnswer(invocation -> invocation.getArgument(0).toString()); // when - Message message = AsyncAnnotationScannerUtil.processMessageFromAnnotation(method); + Message.MessageBuilder actual = Message.builder(); + AsyncAnnotationScannerUtil.processAsyncMessageAnnotation(actual, message, stringResolver); // then var expectedMessage = Message.builder() @@ -147,7 +161,7 @@ void processMessageFromAnnotationWithAsyncMessage(Class classWithOperationBin .schemaFormat("application/schema+json;version=draft-07") .title("Message Title") .build(); - assertEquals(expectedMessage, message); + assertEquals(expectedMessage, actual.build()); } @Test