diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfAutoConfiguration.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfAutoConfiguration.java index ef70e0f8e..0329fb4f6 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfAutoConfiguration.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/SpringwolfAutoConfiguration.java @@ -6,8 +6,11 @@ import io.github.stavshamir.springwolf.asyncapi.ChannelsService; import io.github.stavshamir.springwolf.asyncapi.DefaultAsyncApiService; import io.github.stavshamir.springwolf.asyncapi.DefaultChannelsService; +import io.github.stavshamir.springwolf.asyncapi.DefaultOperationsService; +import io.github.stavshamir.springwolf.asyncapi.OperationsService; import io.github.stavshamir.springwolf.asyncapi.SpringwolfInitApplicationListener; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.OperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.configuration.DefaultAsyncApiDocketService; @@ -58,9 +61,11 @@ public SpringwolfInitApplicationListener springwolfInitApplicationListener( public AsyncApiService asyncApiService( AsyncApiDocketService asyncApiDocketService, ChannelsService channelsService, + OperationsService operationsService, SchemasService schemasService, List customizers) { - return new DefaultAsyncApiService(asyncApiDocketService, channelsService, schemasService, customizers); + return new DefaultAsyncApiService( + asyncApiDocketService, channelsService, operationsService, schemasService, customizers); } @Bean @@ -69,6 +74,12 @@ public ChannelsService channelsService(List channelsS return new DefaultChannelsService(channelsScanners); } + @Bean + @ConditionalOnMissingBean + public OperationsService operationsService(List operationsScanners) { + return new DefaultOperationsService(operationsScanners); + } + @Bean @ConditionalOnMissingBean public SchemasService schemasService( 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 2b193e994..cfdb03118 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 @@ -7,6 +7,8 @@ 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.annotation.AsyncAnnotationOperationsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncAnnotationScanner; 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; @@ -67,7 +69,7 @@ public SpringwolfClassScanner springwolfClassScanner( havingValue = "true", matchIfMissing = true) @Order(value = ChannelPriority.ASYNC_ANNOTATION) - public AsyncAnnotationChannelsScanner asyncListenerAnnotationScanner( + public AsyncAnnotationChannelsScanner asyncListenerAnnotationChannelScanner( SpringwolfClassScanner springwolfClassScanner, SchemasService schemasService, AsyncApiDocketService asyncApiDocketService, @@ -84,13 +86,34 @@ public AsyncAnnotationChannelsScanner asyncListenerAnnotationScan messageBindingProcessors); } + @Bean + @ConditionalOnProperty( + name = SPRINGWOLF_SCANNER_ASYNC_LISTENER_ENABLED, + havingValue = "true", + matchIfMissing = true) + @Order(value = ChannelPriority.ASYNC_ANNOTATION) + public AsyncAnnotationOperationsScanner asyncListenerAnnotationOperationScanner( + SpringwolfClassScanner springwolfClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor, + List operationBindingProcessors, + List messageBindingProcessors) { + return new AsyncAnnotationOperationsScanner<>( + buildAsyncListenerAnnotationProvider(), + springwolfClassScanner, + schemasService, + payloadClassExtractor, + operationBindingProcessors, + messageBindingProcessors); + } + @Bean @ConditionalOnProperty( name = SPRINGWOLF_SCANNER_ASYNC_PUBLISHER_ENABLED, havingValue = "true", matchIfMissing = true) @Order(value = ChannelPriority.ASYNC_ANNOTATION) - public AsyncAnnotationChannelsScanner asyncPublisherAnnotationScanner( + public AsyncAnnotationChannelsScanner asyncPublisherChannelAnnotationScanner( SpringwolfClassScanner springwolfClassScanner, SchemasService schemasService, AsyncApiDocketService asyncApiDocketService, @@ -107,9 +130,30 @@ public AsyncAnnotationChannelsScanner asyncPublisherAnnotationSc messageBindingProcessors); } - private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider + @Bean + @ConditionalOnProperty( + name = SPRINGWOLF_SCANNER_ASYNC_PUBLISHER_ENABLED, + havingValue = "true", + matchIfMissing = true) + @Order(value = ChannelPriority.ASYNC_ANNOTATION) + public AsyncAnnotationOperationsScanner asyncPublisherOperationAnnotationScanner( + SpringwolfClassScanner springwolfClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor, + List operationBindingProcessors, + List messageBindingProcessors) { + return new AsyncAnnotationOperationsScanner<>( + buildAsyncPublisherAnnotationProvider(), + springwolfClassScanner, + schemasService, + payloadClassExtractor, + operationBindingProcessors, + messageBindingProcessors); + } + + private static AsyncAnnotationScanner.AsyncAnnotationProvider buildAsyncListenerAnnotationProvider() { - return new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { + return new AsyncAnnotationScanner.AsyncAnnotationProvider<>() { @Override public Class getAnnotation() { return AsyncListener.class; @@ -127,9 +171,9 @@ public OperationAction getOperationType() { }; } - private static AsyncAnnotationChannelsScanner.AsyncAnnotationProvider + private static AsyncAnnotationScanner.AsyncAnnotationProvider buildAsyncPublisherAnnotationProvider() { - return new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { + return new AsyncAnnotationScanner.AsyncAnnotationProvider<>() { @Override public Class getAnnotation() { return AsyncPublisher.class; diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/ChannelsService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/ChannelsService.java index 611277817..fe2ce0020 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/ChannelsService.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/ChannelsService.java @@ -2,7 +2,6 @@ package io.github.stavshamir.springwolf.asyncapi; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; import java.util.Map; @@ -17,11 +16,4 @@ public interface ChannelsService { * @return Map of channel names mapping to detected ChannelItems */ Map findChannels(); - - /** - * Detects all available AsyncAPI Operation in the spring context. - * - * @return Map of operation names mapping to detected Operations - */ - Map findOperations(); } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java index f22208506..dc9aa4206 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java @@ -28,6 +28,7 @@ private record AsyncAPIResult(AsyncAPI asyncAPI, Throwable exception) {} private final AsyncApiDocketService asyncApiDocketService; private final ChannelsService channelsService; + private final OperationsService operationsService; private final SchemasService schemasService; private final List customizers; @@ -65,7 +66,7 @@ protected synchronized void initAsyncAPI() { // SchemasService. Map channels = channelsService.findChannels(); - Map operations = channelsService.findOperations(); + Map operations = operationsService.findOperations(); Components components = Components.builder() .schemas(schemasService.getSchemas()) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsService.java index db70731a8..5b25ef5e5 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsService.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsService.java @@ -4,7 +4,6 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelMerger; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,7 +31,7 @@ public Map findChannels() { for (ChannelsScanner scanner : channelsScanners) { try { - Map channels = scanner.scanChannels(); + Map channels = scanner.scan(); foundChannelItems.addAll(channels.entrySet()); } catch (Exception e) { log.error("An error was encountered during channel scanning with {}: {}", scanner, e.getMessage(), e); @@ -40,24 +39,4 @@ public Map findChannels() { } return ChannelMerger.mergeChannels(foundChannelItems); } - - /** - * Collects all AsyncAPI Operation using the available {@link ChannelsScanner} - * beans. - * @return Map of operation names mapping to detected Operation - */ - @Override - public Map findOperations() { - List> foundOperations = new ArrayList<>(); - for (ChannelsScanner scanner : channelsScanners) { - try { - Map channels = scanner.scanOperations(); - foundOperations.addAll(channels.entrySet()); - } catch (Exception e) { - log.error("An error was encountered during operation scanning with {}: {}", scanner, e.getMessage(), e); - } - } - - return ChannelMerger.mergeOperations(foundOperations); - } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultOperationsService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultOperationsService.java new file mode 100644 index 000000000..22194e085 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultOperationsService.java @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi; + +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.OperationMerger; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.OperationsScanner; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Service to detect AsyncAPI operations in the current spring context. + */ +@Slf4j +@RequiredArgsConstructor +public class DefaultOperationsService implements OperationsService { + + private final List operationScanners; + + /** + * Collects all AsyncAPI Operation using the available {@link OperationsScanner} + * beans. + * @return Map of operation names mapping to detected Operation + */ + @Override + public Map findOperations() { + List> foundOperations = new ArrayList<>(); + + for (OperationsScanner scanner : operationScanners) { + try { + Map channels = scanner.scan(); + foundOperations.addAll(channels.entrySet()); + } catch (Exception e) { + log.error("An error was encountered during operation scanning with {}: {}", scanner, e.getMessage(), e); + } + } + return OperationMerger.mergeOperations(foundOperations); + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/OperationsService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/OperationsService.java new file mode 100644 index 000000000..059af34fb --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/OperationsService.java @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi; + +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; + +import java.util.Map; + +/** + * Service to detect AsyncAPI channels in the current spring context. + */ +public interface OperationsService { + + /** + * Detects all available AsyncAPI Operation in the spring context. + * + * @return Map of operation names mapping to detected Operations + */ + Map findOperations(); +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMerger.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMerger.java index 2f21e0e94..b82da5f66 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMerger.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMerger.java @@ -4,12 +4,8 @@ import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.Channel; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.Message; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; -import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; @@ -44,31 +40,6 @@ public static Map mergeChannels(List - * Given two operations for the same operation name, the first seen Operation is used - * If an operation is null, the next non-null operation is used - * Messages within operations are merged - * - * @param operationEntries Ordered pairs of operation name to Operation - * @return A map of operationId to a single Operation - */ - public static Map mergeOperations(List> operationEntries) { - Map mergedOperations = new HashMap<>(); - - for (Map.Entry entry : operationEntries) { - if (!mergedOperations.containsKey(entry.getKey())) { - mergedOperations.put(entry.getKey(), entry.getValue()); - } else { - Operation operation = mergeOperation(mergedOperations.get(entry.getKey()), entry.getValue()); - mergedOperations.put(entry.getKey(), operation); - } - } - - return mergedOperations; - } - private static ChannelObject mergeChannel(ChannelObject channel, ChannelObject otherChannel) { ChannelObject mergedChannel = channel != null ? channel : otherChannel; @@ -89,27 +60,4 @@ private static ChannelObject mergeChannel(ChannelObject channel, ChannelObject o return mergedChannel; } - - private static Operation mergeOperation(Operation operation, Operation otherOperation) { - Operation mergedOperation = operation != null ? operation : otherOperation; - - List mergedMessages = - mergeMessageReferences(operation.getMessages(), otherOperation.getMessages()); - if (!mergedMessages.isEmpty()) { - mergedOperation.setMessages(mergedMessages); - } - return mergedOperation; - } - - private static List mergeMessageReferences( - Collection messages, Collection otherMessages) { - var messageReferences = new HashSet(); - if (messages != null) { - messageReferences.addAll(messages); - } - if (otherMessages != null) { - messageReferences.addAll(otherMessages); - } - return messageReferences.stream().toList(); - } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelsScanner.java index 9ca09deb5..6528eb439 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelsScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelsScanner.java @@ -2,7 +2,6 @@ package io.github.stavshamir.springwolf.asyncapi.scanners.channels; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; import java.util.Map; @@ -11,10 +10,5 @@ public interface ChannelsScanner { /** * @return A mapping of channel names to their respective channel object for a given protocol. */ - Map scanChannels(); - - /** - * @return A mapping of operation names to their respective operation object for a given protocol. - */ - Map scanOperations(); + Map scan(); } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationMerger.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationMerger.java new file mode 100644 index 000000000..e3e70b20e --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationMerger.java @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels; + +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.Channel; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * Util to merge multiple {@link Channel}s + */ +public class OperationMerger { + + private OperationMerger() {} + + /** + * Merges multiple operations by operation name + *

+ * Given two operations for the same operation name, the first seen Operation is used + * If an operation is null, the next non-null operation is used + * Messages within operations are merged + * + * @param operationEntries Ordered pairs of operation name to Operation + * @return A map of operationId to a single Operation + */ + public static Map mergeOperations(List> operationEntries) { + Map mergedOperations = new HashMap<>(); + + for (Map.Entry entry : operationEntries) { + if (!mergedOperations.containsKey(entry.getKey())) { + mergedOperations.put(entry.getKey(), entry.getValue()); + } else { + Operation operation = mergeOperation(mergedOperations.get(entry.getKey()), entry.getValue()); + mergedOperations.put(entry.getKey(), operation); + } + } + + return mergedOperations; + } + + private static Operation mergeOperation(Operation operation, Operation otherOperation) { + Operation mergedOperation = operation != null ? operation : otherOperation; + + List mergedMessages = + mergeMessageReferences(operation.getMessages(), otherOperation.getMessages()); + if (!mergedMessages.isEmpty()) { + mergedOperation.setMessages(mergedMessages); + } + return mergedOperation; + } + + private static List mergeMessageReferences( + Collection messages, Collection otherMessages) { + var messageReferences = new HashSet(); + if (messages != null) { + messageReferences.addAll(messages); + } + if (otherMessages != null) { + messageReferences.addAll(otherMessages); + } + return messageReferences.stream().toList(); + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationsScanner.java new file mode 100644 index 000000000..0e8b1c9e2 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationsScanner.java @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels; + +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; + +import java.util.Map; + +public interface OperationsScanner { + + /** + * @return A mapping of operation names to their respective operation object for a given protocol. + */ + Map scan(); +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScanner.java index c6729a139..b5aa8bfdd 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScanner.java @@ -3,7 +3,6 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; import lombok.RequiredArgsConstructor; import java.util.List; @@ -19,7 +18,7 @@ public class SimpleChannelsScanner implements ChannelsScanner { private final ClassProcessor classProcessor; @Override - public Map scanChannels() { + public Map scan() { Set> components = classScanner.scan(); List> channels = mapToChannels(components); @@ -27,26 +26,11 @@ public Map scanChannels() { return ChannelMerger.mergeChannels(channels); } - @Override - public Map scanOperations() { - Set> components = classScanner.scan(); - - List> operations = mapToOperations(components); - - return ChannelMerger.mergeOperations(operations); - } - private List> mapToChannels(Set> components) { - return components.stream().flatMap(classProcessor::processChannels).toList(); - } - - private List> mapToOperations(Set> components) { - return components.stream().flatMap(classProcessor::processOperations).toList(); + return components.stream().flatMap(classProcessor::process).toList(); } public interface ClassProcessor { - Stream> processChannels(Class clazz); - - Stream> processOperations(Class clazz); + Stream> process(Class clazz); } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleOperationsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleOperationsScanner.java new file mode 100644 index 000000000..b8b986df2 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleOperationsScanner.java @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels; + +import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class SimpleOperationsScanner implements OperationsScanner { + + private final ClassScanner classScanner; + + private final ClassProcessor classProcessor; + + @Override + public Map scan() { + Set> components = classScanner.scan(); + + List> operations = mapToOperations(components); + + return OperationMerger.mergeOperations(operations); + } + + private List> mapToOperations(Set> components) { + return components.stream().flatMap(classProcessor::process).toList(); + } + + public interface ClassProcessor { + Stream> process(Class clazz); + } +} 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 4c3fd07a6..79aa4b518 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 @@ -8,59 +8,47 @@ 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.channel.operation.message.header.AsyncHeaders; -import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; -import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ServerReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; -import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; -import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; import io.github.stavshamir.springwolf.asyncapi.v3.model.server.Server; 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.stream.Stream; @Slf4j -@RequiredArgsConstructor -public class AsyncAnnotationChannelsScanner - implements ChannelsScanner, EmbeddedValueResolverAware { +public class AsyncAnnotationChannelsScanner extends AsyncAnnotationScanner + implements ChannelsScanner { - 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; + public AsyncAnnotationChannelsScanner( + AsyncAnnotationProvider asyncAnnotationProvider, + ClassScanner classScanner, + SchemasService schemasService, + AsyncApiDocketService asyncApiDocketService, + PayloadClassExtractor payloadClassExtractor, + List operationBindingProcessors, + List messageBindingProcessors) { + super( + asyncAnnotationProvider, + payloadClassExtractor, + schemasService, + operationBindingProcessors, + messageBindingProcessors); + this.classScanner = classScanner; + this.asyncApiDocketService = asyncApiDocketService; } @Override - public Map scanChannels() { + public Map scan() { List> channels = classScanner.scan().stream() .flatMap(this::getAnnotatedMethods) .map(this::buildChannel) @@ -69,28 +57,6 @@ public Map scanChannels() { return ChannelMerger.mergeChannels(channels); } - @Override - public Map scanOperations() { - List> operations = classScanner.scan().stream() - .flatMap(this::getAnnotatedMethods) - .map(this::buildOperation) - .toList(); - - return ChannelMerger.mergeOperations(operations); - } - - 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 Map.Entry buildChannel(MethodAndAnnotation methodAndAnnotation) { ChannelObject.ChannelObjectBuilder channelBuilder = ChannelObject.builder(); @@ -115,79 +81,6 @@ private Map.Entry buildChannel(MethodAndAnnotation met return Map.entry(channelName, channelItem); } - private Map.Entry buildOperation(MethodAndAnnotation methodAndAnnotation) { - AsyncOperation operationAnnotation = - this.asyncAnnotationProvider.getAsyncOperation(methodAndAnnotation.annotation()); - String channelName = resolver.resolveStringValue(operationAnnotation.channelName()); - String operationId = channelName + "_" + this.asyncAnnotationProvider.getOperationType().type + "_" - + methodAndAnnotation.method.getName(); - - Operation operation = buildOperation(operationAnnotation, methodAndAnnotation.method(), channelName); - operation.setAction(this.asyncAnnotationProvider.getOperationType()); - - return Map.entry(operationId, operation); - } - - 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 operationTitle = channelName + "_" + this.asyncAnnotationProvider.getOperationType().type; - - Map operationBinding = - AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors); - Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; - MessageObject message = buildMessage(asyncOperation, method); - - return Operation.builder() - .channel(ChannelReference.fromChannel(channelName)) - .description(description) - .title(operationTitle) - .messages(List.of(MessageReference.toChannelMessage(channelName, message))) - .bindings(opBinding) - .build(); - } - - private MessageObject buildMessage(AsyncOperation operationData, Method method) { - Class payloadType = operationData.payloadType() != Object.class - ? operationData.payloadType() - : payloadClassExtractor.extractFrom(method); - - String modelName = this.schemasService.registerSchema(payloadType); - AsyncHeaders asyncHeaders = AsyncAnnotationScannerUtil.getAsyncHeaders(operationData, resolver); - String headerModelName = this.schemasService.registerSchema(asyncHeaders); - var headers = MessageHeaders.of(MessageReference.toSchema(headerModelName)); - - var schema = payloadType.getAnnotation(Schema.class); - String description = schema != null ? schema.description() : null; - - Map messageBinding = - AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors); - - var messagePayload = MessagePayload.of(MultiFormatSchema.builder() - .schema(SchemaReference.fromSchema(modelName)) - .build()); - - var builder = MessageObject.builder() - .messageId(payloadType.getName()) - .name(payloadType.getName()) - .title(payloadType.getSimpleName()) - .description(description) - .payload(messagePayload) - .headers(headers) - .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. - AsyncAnnotationScannerUtil.processAsyncMessageAnnotation(builder, operationData.message(), this.resolver); - - MessageObject message = builder.build(); - this.schemasService.registerMessage(message); - return message; - } - /** * validates the given list of server names (for a specific operation) with the servers defined in the 'servers' part of * the current AsyncApi. @@ -214,14 +107,4 @@ void validateServers(List serversFromOperation, String operationId) { } } } - - public interface AsyncAnnotationProvider { - Class getAnnotation(); - - AsyncOperation getAsyncOperation(A annotation); - - OperationAction getOperationType(); - } - - private record MethodAndAnnotation(Method method, A annotation) {} } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationOperationsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationOperationsScanner.java new file mode 100644 index 000000000..5f73dfa76 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationOperationsScanner.java @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +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.OperationMerger; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.OperationsScanner; +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.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import lombok.extern.slf4j.Slf4j; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; + +@Slf4j +public class AsyncAnnotationOperationsScanner extends AsyncAnnotationScanner + implements OperationsScanner { + + private final ClassScanner classScanner; + + public AsyncAnnotationOperationsScanner( + AsyncAnnotationProvider asyncAnnotationProvider, + ClassScanner classScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor, + List operationBindingProcessors, + List messageBindingProcessors) { + super( + asyncAnnotationProvider, + payloadClassExtractor, + schemasService, + operationBindingProcessors, + messageBindingProcessors); + this.classScanner = classScanner; + } + + @Override + public Map scan() { + List> operations = classScanner.scan().stream() + .flatMap(this::getAnnotatedMethods) + .map(this::buildOperation) + .toList(); + + return OperationMerger.mergeOperations(operations); + } + + private Map.Entry buildOperation(MethodAndAnnotation methodAndAnnotation) { + AsyncOperation operationAnnotation = + this.asyncAnnotationProvider.getAsyncOperation(methodAndAnnotation.annotation()); + String channelName = resolver.resolveStringValue(operationAnnotation.channelName()); + String operationId = channelName + "_" + this.asyncAnnotationProvider.getOperationType().type + "_" + + methodAndAnnotation.method().getName(); + + Operation operation = buildOperation(operationAnnotation, methodAndAnnotation.method(), channelName); + operation.setAction(this.asyncAnnotationProvider.getOperationType()); + + return Map.entry(operationId, operation); + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScanner.java new file mode 100644 index 000000000..a581cee71 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationScanner.java @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +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.operationdata.annotation.AsyncOperation; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; +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.stream.Stream; + +@Slf4j +@RequiredArgsConstructor +public abstract class AsyncAnnotationScanner implements EmbeddedValueResolverAware { + + protected final AsyncAnnotationProvider asyncAnnotationProvider; + protected final PayloadClassExtractor payloadClassExtractor; + protected final SchemasService schemasService; + protected final List operationBindingProcessors; + protected final List messageBindingProcessors; + protected StringValueResolver resolver; + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.resolver = resolver; + } + + protected 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))); + } + + protected Operation buildOperation(AsyncOperation asyncOperation, Method method, String channelName) { + String description = this.resolver.resolveStringValue(asyncOperation.description()); + if (!StringUtils.hasText(description)) { + description = "Auto-generated description"; + } + + String operationTitle = channelName + "_" + this.asyncAnnotationProvider.getOperationType().type; + + Map operationBinding = + AsyncAnnotationScannerUtil.processOperationBindingFromAnnotation(method, operationBindingProcessors); + Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; + MessageObject message = buildMessage(asyncOperation, method); + + return Operation.builder() + .channel(ChannelReference.fromChannel(channelName)) + .description(description) + .title(operationTitle) + .messages(List.of(MessageReference.toChannelMessage(channelName, message))) + .bindings(opBinding) + .build(); + } + + protected MessageObject buildMessage(AsyncOperation operationData, Method method) { + Class payloadType = operationData.payloadType() != Object.class + ? operationData.payloadType() + : payloadClassExtractor.extractFrom(method); + + String modelName = this.schemasService.registerSchema(payloadType); + AsyncHeaders asyncHeaders = AsyncAnnotationScannerUtil.getAsyncHeaders(operationData, resolver); + String headerModelName = this.schemasService.registerSchema(asyncHeaders); + var headers = MessageHeaders.of(MessageReference.toSchema(headerModelName)); + + var schema = payloadType.getAnnotation(Schema.class); + String description = schema != null ? schema.description() : null; + + Map messageBinding = + AsyncAnnotationScannerUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors); + + var messagePayload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(modelName)) + .build()); + + var builder = MessageObject.builder() + .messageId(payloadType.getName()) + .name(payloadType.getName()) + .title(payloadType.getSimpleName()) + .description(description) + .payload(messagePayload) + .headers(headers) + .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. + AsyncAnnotationScannerUtil.processAsyncMessageAnnotation(builder, operationData.message(), this.resolver); + + MessageObject message = builder.build(); + this.schemasService.registerMessage(message); + return message; + } + + public interface AsyncAnnotationProvider { + Class getAnnotation(); + + AsyncOperation getAsyncOperation(A annotation); + + OperationAction getOperationType(); + } + + protected record MethodAndAnnotation(Method method, A annotation) {} +} 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 8952e9c88..6ac10900d 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 @@ -6,85 +6,48 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersBuilder; import io.github.stavshamir.springwolf.asyncapi.v3.bindings.ChannelBinding; -import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; -import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; -import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; -import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; import io.github.stavshamir.springwolf.schemas.SchemasService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.annotation.AnnotationUtils; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.stream.Stream; -import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessagesMap; -import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toOperationsMessagesMap; -import static java.util.stream.Collectors.toSet; - -@RequiredArgsConstructor @Slf4j public class ClassLevelAnnotationChannelsScanner< ClassAnnotation extends Annotation, MethodAnnotation extends Annotation> + extends ClassLevelAnnotationScanner implements SimpleChannelsScanner.ClassProcessor { - private final Class classAnnotationClass; - private final Class methodAnnotationClass; - private final BindingFactory bindingFactory; - private final AsyncHeadersBuilder asyncHeadersBuilder; - private final PayloadClassExtractor payloadClassExtractor; - private final SchemasService schemasService; - - private enum MessageType { - CHANNEL, - OPERATION; + public ClassLevelAnnotationChannelsScanner( + Class classAnnotationClass, + Class methodAnnotationClass, + BindingFactory bindingFactory, + AsyncHeadersBuilder asyncHeadersBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + super( + classAnnotationClass, + methodAnnotationClass, + bindingFactory, + asyncHeadersBuilder, + payloadClassExtractor, + schemasService); } @Override - public Stream> processChannels(Class clazz) { + public Stream> process(Class clazz) { log.debug( "Scanning class \"{}\" for @\"{}\" annotated methods", clazz.getName(), classAnnotationClass.getName()); return Stream.of(clazz).filter(this::isClassAnnotated).flatMap(this::mapClassToChannel); } - @Override - public Stream> processOperations(Class clazz) { - log.debug( - "Scanning class \"{}\" for @\"{}\" annotated methods", clazz.getName(), classAnnotationClass.getName()); - - return Stream.of(clazz).filter(this::isClassAnnotated).flatMap(this::mapClassToOperation); - } - - private boolean isClassAnnotated(Class component) { - return AnnotationUtil.findAnnotation(classAnnotationClass, component) != null; - } - - private Set getAnnotatedMethods(Class clazz) { - log.debug( - "Scanning class \"{}\" for @\"{}\" annotated methods", - clazz.getName(), - methodAnnotationClass.getName()); - - return Arrays.stream(clazz.getDeclaredMethods()) - .filter(method -> !method.isBridge()) - .filter(method -> AnnotationUtils.findAnnotation(method, methodAnnotationClass) != null) - .collect(toSet()); - } - private Stream> mapClassToChannel(Class component) { log.debug("Mapping class \"{}\" to channels", component.getName()); @@ -102,73 +65,11 @@ private Stream> mapClassToChannel(Class comp return Stream.of(Map.entry(channelName, channelItem)); } - private Stream> mapClassToOperation(Class component) { - log.debug("Mapping class \"{}\" to operations", component.getName()); - - ClassAnnotation classAnnotation = AnnotationUtil.findAnnotationOrThrow(classAnnotationClass, component); - - Set annotatedMethods = getAnnotatedMethods(component); - if (annotatedMethods.isEmpty()) { - return Stream.empty(); - } - - String channelName = bindingFactory.getChannelName(classAnnotation); - String operationId = channelName + "_" + OperationAction.RECEIVE + "_" + component.getSimpleName(); - - Operation operation = buildOperation(classAnnotation, annotatedMethods); - - return Stream.of(Map.entry(operationId, operation)); - } - private ChannelObject buildChannelItem(ClassAnnotation classAnnotation, Set methods) { var messages = buildMessages(classAnnotation, methods, MessageType.CHANNEL); return buildChannelItem(classAnnotation, messages); } - private Operation buildOperation(ClassAnnotation classAnnotation, Set methods) { - var messages = buildMessages(classAnnotation, methods, MessageType.OPERATION); - return buildOperation(classAnnotation, messages); - } - - private Map buildMessages( - ClassAnnotation classAnnotation, Set methods, MessageType messageType) { - Set messages = methods.stream() - .map((Method method) -> { - Class payloadType = payloadClassExtractor.extractFrom(method); - return buildMessage(classAnnotation, payloadType); - }) - .collect(toSet()); - - if (messageType == MessageType.OPERATION) { - String channelName = bindingFactory.getChannelName(classAnnotation); - return toOperationsMessagesMap(channelName, messages); - } - return toMessagesMap(messages); - } - - private MessageObject buildMessage(ClassAnnotation classAnnotation, Class payloadType) { - Map messageBinding = bindingFactory.buildMessageBinding(classAnnotation); - String modelName = schemasService.registerSchema(payloadType); - String headerModelName = schemasService.registerSchema(asyncHeadersBuilder.buildHeaders(payloadType)); - - MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() - .schema(SchemaReference.fromSchema(modelName)) - .build()); - - MessageObject message = MessageObject.builder() - .messageId(payloadType.getName()) - .name(payloadType.getName()) - .title(payloadType.getSimpleName()) - .description(null) - .payload(payload) - .headers(MessageHeaders.of(MessageReference.toSchema(headerModelName))) - .bindings(messageBinding) - .build(); - - this.schemasService.registerMessage(message); - return message; - } - private ChannelObject buildChannelItem(ClassAnnotation classAnnotation, Map messages) { Map channelBinding = bindingFactory.buildChannelBinding(classAnnotation); Map chBinding = channelBinding != null ? new HashMap<>(channelBinding) : null; @@ -177,17 +78,4 @@ private ChannelObject buildChannelItem(ClassAnnotation classAnnotation, Map(messages)) .build(); } - - private Operation buildOperation(ClassAnnotation classAnnotation, Map messages) { - Map operationBinding = bindingFactory.buildOperationBinding(classAnnotation); - Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; - String channelName = bindingFactory.getChannelName(classAnnotation); - - return Operation.builder() - .action(OperationAction.RECEIVE) - .channel(ChannelReference.fromChannel(channelName)) - .messages(messages.values().stream().toList()) - .bindings(opBinding) - .build(); - } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationOperationsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationOperationsScanner.java new file mode 100644 index 000000000..31700cbb6 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationOperationsScanner.java @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.BindingFactory; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleOperationsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersBuilder; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import lombok.extern.slf4j.Slf4j; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +@Slf4j +public class ClassLevelAnnotationOperationsScanner< + ClassAnnotation extends Annotation, MethodAnnotation extends Annotation> + extends ClassLevelAnnotationScanner + implements SimpleOperationsScanner.ClassProcessor { + + public ClassLevelAnnotationOperationsScanner( + Class classAnnotationClass, + Class methodAnnotationClass, + BindingFactory bindingFactory, + AsyncHeadersBuilder asyncHeadersBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + super( + classAnnotationClass, + methodAnnotationClass, + bindingFactory, + asyncHeadersBuilder, + payloadClassExtractor, + schemasService); + } + + @Override + public Stream> process(Class clazz) { + log.debug( + "Scanning class \"{}\" for @\"{}\" annotated methods", clazz.getName(), classAnnotationClass.getName()); + + return Stream.of(clazz).filter(this::isClassAnnotated).flatMap(this::mapClassToOperation); + } + + private Stream> mapClassToOperation(Class component) { + log.debug("Mapping class \"{}\" to operations", component.getName()); + + ClassAnnotation classAnnotation = AnnotationUtil.findAnnotationOrThrow(classAnnotationClass, component); + + Set annotatedMethods = getAnnotatedMethods(component); + if (annotatedMethods.isEmpty()) { + return Stream.empty(); + } + + String channelName = bindingFactory.getChannelName(classAnnotation); + String operationId = channelName + "_" + OperationAction.RECEIVE + "_" + component.getSimpleName(); + + Operation operation = buildOperation(classAnnotation, annotatedMethods); + + return Stream.of(Map.entry(operationId, operation)); + } + + private Operation buildOperation(ClassAnnotation classAnnotation, Set methods) { + var messages = buildMessages(classAnnotation, methods, MessageType.OPERATION); + return buildOperation(classAnnotation, messages); + } + + private Operation buildOperation(ClassAnnotation classAnnotation, Map messages) { + Map operationBinding = bindingFactory.buildOperationBinding(classAnnotation); + Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; + String channelName = bindingFactory.getChannelName(classAnnotation); + + return Operation.builder() + .action(OperationAction.RECEIVE) + .channel(ChannelReference.fromChannel(channelName)) + .messages(messages.values().stream().toList()) + .bindings(opBinding) + .build(); + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationScanner.java new file mode 100644 index 000000000..9c6147b8e --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationScanner.java @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.BindingFactory; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersBuilder; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; + +import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessagesMap; +import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toOperationsMessagesMap; +import static java.util.stream.Collectors.toSet; + +@Slf4j +@RequiredArgsConstructor +public abstract class ClassLevelAnnotationScanner< + ClassAnnotation extends Annotation, MethodAnnotation extends Annotation> { + + protected final Class classAnnotationClass; + protected final Class methodAnnotationClass; + protected final BindingFactory bindingFactory; + protected final AsyncHeadersBuilder asyncHeadersBuilder; + protected final PayloadClassExtractor payloadClassExtractor; + protected final SchemasService schemasService; + + protected enum MessageType { + CHANNEL, + OPERATION; + } + + protected boolean isClassAnnotated(Class component) { + return AnnotationUtil.findAnnotation(classAnnotationClass, component) != null; + } + + protected Set getAnnotatedMethods(Class clazz) { + log.debug( + "Scanning class \"{}\" for @\"{}\" annotated methods", + clazz.getName(), + methodAnnotationClass.getName()); + + return Arrays.stream(clazz.getDeclaredMethods()) + .filter(method -> !method.isBridge()) + .filter(method -> AnnotationUtils.findAnnotation(method, methodAnnotationClass) != null) + .collect(toSet()); + } + + protected Map buildMessages( + ClassAnnotation classAnnotation, + Set methods, + ClassLevelAnnotationOperationsScanner.MessageType messageType) { + Set messages = methods.stream() + .map((Method method) -> { + Class payloadType = payloadClassExtractor.extractFrom(method); + return buildMessage(classAnnotation, payloadType); + }) + .collect(toSet()); + + if (messageType == MessageType.OPERATION) { + String channelName = bindingFactory.getChannelName(classAnnotation); + return toOperationsMessagesMap(channelName, messages); + } + return toMessagesMap(messages); + } + + protected MessageObject buildMessage(ClassAnnotation classAnnotation, Class payloadType) { + Map messageBinding = bindingFactory.buildMessageBinding(classAnnotation); + String modelName = schemasService.registerSchema(payloadType); + String headerModelName = schemasService.registerSchema(asyncHeadersBuilder.buildHeaders(payloadType)); + + MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(modelName)) + .build()); + + MessageObject message = MessageObject.builder() + .messageId(payloadType.getName()) + .name(payloadType.getName()) + .title(payloadType.getSimpleName()) + .description(null) + .payload(payload) + .headers(MessageHeaders.of(MessageReference.toSchema(headerModelName))) + .bindings(messageBinding) + .build(); + + this.schemasService.registerMessage(message); + return message; + } +} 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 02c226807..7605d91f7 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 @@ -4,57 +4,39 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.BindingFactory; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleChannelsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import io.github.stavshamir.springwolf.asyncapi.v3.bindings.ChannelBinding; -import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; -import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; -import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; -import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; import io.github.stavshamir.springwolf.schemas.SchemasService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.stream.Stream; -@RequiredArgsConstructor @Slf4j public class MethodLevelAnnotationChannelsScanner - implements SimpleChannelsScanner.ClassProcessor { + extends MethodLevelAnnotationScanner implements SimpleChannelsScanner.ClassProcessor { private final Class methodAnnotationClass; - private final BindingFactory bindingFactory; private final PayloadClassExtractor payloadClassExtractor; - private final SchemasService schemasService; - @Override - public Stream> processChannels(Class clazz) { - log.debug( - "Scanning class \"{}\" for @\"{}\" annotated methods", - clazz.getName(), - methodAnnotationClass.getName()); - - return Arrays.stream(clazz.getDeclaredMethods()) - .filter(method -> !method.isBridge()) - .filter(method -> AnnotationUtil.findAnnotation(methodAnnotationClass, method) != null) - .map(this::mapMethodToChannel); + public MethodLevelAnnotationChannelsScanner( + Class methodAnnotationClass, + BindingFactory bindingFactory, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + super(bindingFactory, schemasService); + this.methodAnnotationClass = methodAnnotationClass; + this.payloadClassExtractor = payloadClassExtractor; } @Override - public Stream> processOperations(Class clazz) { + public Stream> process(Class clazz) { log.debug( "Scanning class \"{}\" for @\"{}\" annotated methods", clazz.getName(), @@ -63,7 +45,7 @@ public Stream> processOperations(Class clazz) { return Arrays.stream(clazz.getDeclaredMethods()) .filter(method -> !method.isBridge()) .filter(method -> AnnotationUtil.findAnnotation(methodAnnotationClass, method) != null) - .map(this::mapMethodToOperation); + .map(this::mapMethodToChannel); } private Map.Entry mapMethodToChannel(Method method) { @@ -79,51 +61,11 @@ private Map.Entry mapMethodToChannel(Method method) { return Map.entry(channelName, channelItem); } - private Map.Entry mapMethodToOperation(Method method) { - log.debug("Mapping method \"{}\" to operations", method.getName()); - - MethodAnnotation annotation = AnnotationUtil.findAnnotationOrThrow(methodAnnotationClass, method); - - String channelName = bindingFactory.getChannelName(annotation); - String operationId = channelName + "_" + OperationAction.RECEIVE + "_" + method.getName(); - Class payload = payloadClassExtractor.extractFrom(method); - - Operation operation = buildOperation(annotation, payload); - return Map.entry(operationId, operation); - } - private ChannelObject buildChannelItem(MethodAnnotation annotation, Class payloadType) { MessageObject message = buildMessage(annotation, payloadType); return buildChannelItem(annotation, message); } - private Operation buildOperation(MethodAnnotation annotation, Class payloadType) { - MessageObject message = buildMessage(annotation, payloadType); - return buildOperation(annotation, message); - } - - private MessageObject buildMessage(MethodAnnotation annotation, Class payloadType) { - Map messageBinding = bindingFactory.buildMessageBinding(annotation); - String modelName = schemasService.registerSchema(payloadType); - String headerModelName = schemasService.registerSchema(AsyncHeaders.NOT_DOCUMENTED); - MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() - .schema(SchemaReference.fromSchema(modelName)) - .build()); - - MessageObject message = MessageObject.builder() - .messageId(payloadType.getName()) - .name(payloadType.getName()) - .title(payloadType.getSimpleName()) - .description(null) - .payload(payload) - .headers(MessageHeaders.of(MessageReference.toSchema(headerModelName))) - .bindings(messageBinding) - .build(); - - this.schemasService.registerMessage(message); - return message; - } - private ChannelObject buildChannelItem(MethodAnnotation annotation, MessageObject message) { Map channelBinding = bindingFactory.buildChannelBinding(annotation); Map chBinding = channelBinding != null ? new HashMap<>(channelBinding) : null; @@ -132,17 +74,4 @@ private ChannelObject buildChannelItem(MethodAnnotation annotation, MessageObjec .bindings(chBinding) .build(); } - - private Operation buildOperation(MethodAnnotation annotation, MessageObject message) { - Map operationBinding = bindingFactory.buildOperationBinding(annotation); - Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; - String channelName = bindingFactory.getChannelName(annotation); - - return Operation.builder() - .action(OperationAction.RECEIVE) - .channel(ChannelReference.fromChannel(channelName)) - .messages(List.of(MessageReference.toChannelMessage(channelName, message))) - .bindings(opBinding) - .build(); - } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationOperationsScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationOperationsScanner.java new file mode 100644 index 000000000..18ccd4337 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationOperationsScanner.java @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.BindingFactory; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleOperationsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import lombok.extern.slf4j.Slf4j; + +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.stream.Stream; + +@Slf4j +public class MethodLevelAnnotationOperationsScanner + extends MethodLevelAnnotationScanner implements SimpleOperationsScanner.ClassProcessor { + + private final Class methodAnnotationClass; + private final PayloadClassExtractor payloadClassExtractor; + + public MethodLevelAnnotationOperationsScanner( + Class methodAnnotationClass, + BindingFactory bindingFactory, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + super(bindingFactory, schemasService); + this.methodAnnotationClass = methodAnnotationClass; + this.payloadClassExtractor = payloadClassExtractor; + } + + @Override + public Stream> process(Class clazz) { + log.debug( + "Scanning class \"{}\" for @\"{}\" annotated methods", + clazz.getName(), + methodAnnotationClass.getName()); + + return Arrays.stream(clazz.getDeclaredMethods()) + .filter(method -> !method.isBridge()) + .filter(method -> AnnotationUtil.findAnnotation(methodAnnotationClass, method) != null) + .map(this::mapMethodToOperation); + } + + private Map.Entry mapMethodToOperation(Method method) { + log.debug("Mapping method \"{}\" to operations", method.getName()); + + MethodAnnotation annotation = AnnotationUtil.findAnnotationOrThrow(methodAnnotationClass, method); + + String channelName = bindingFactory.getChannelName(annotation); + String operationId = channelName + "_" + OperationAction.RECEIVE + "_" + method.getName(); + Class payload = payloadClassExtractor.extractFrom(method); + + Operation operation = buildOperation(annotation, payload); + return Map.entry(operationId, operation); + } + + private Operation buildOperation(MethodAnnotation annotation, Class payloadType) { + MessageObject message = buildMessage(annotation, payloadType); + return buildOperation(annotation, message); + } + + private Operation buildOperation(MethodAnnotation annotation, MessageObject message) { + Map operationBinding = bindingFactory.buildOperationBinding(annotation); + Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; + String channelName = bindingFactory.getChannelName(annotation); + + return Operation.builder() + .action(OperationAction.RECEIVE) + .channel(ChannelReference.fromChannel(channelName)) + .messages(List.of(MessageReference.toChannelMessage(channelName, message))) + .bindings(opBinding) + .build(); + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationScanner.java new file mode 100644 index 000000000..ef8bbf332 --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationScanner.java @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.BindingFactory; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.lang.annotation.Annotation; +import java.util.Map; + +@RequiredArgsConstructor +@Slf4j +public abstract class MethodLevelAnnotationScanner { + + protected final BindingFactory bindingFactory; + protected final SchemasService schemasService; + + protected MessageObject buildMessage(MethodAnnotation annotation, Class payloadType) { + Map messageBinding = bindingFactory.buildMessageBinding(annotation); + String modelName = schemasService.registerSchema(payloadType); + String headerModelName = schemasService.registerSchema(AsyncHeaders.NOT_DOCUMENTED); + MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(modelName)) + .build()); + + MessageObject message = MessageObject.builder() + .messageId(payloadType.getName()) + .name(payloadType.getName()) + .title(payloadType.getSimpleName()) + .description(null) + .payload(payload) + .headers(MessageHeaders.of(MessageReference.toSchema(headerModelName))) + .bindings(messageBinding) + .build(); + + this.schemasService.registerMessage(message); + return message; + } +} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceIntegrationTest.java index 8c181b38e..caa5927bb 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceIntegrationTest.java @@ -55,6 +55,9 @@ class DefaultAsyncApiServiceIntegrationTest { @MockBean private ChannelsService channelsService; + @MockBean + private OperationsService operationsService; + @MockBean private SchemasService schemasService; diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceTest.java index 7ea90ccd6..172d22dc4 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiServiceTest.java @@ -26,6 +26,7 @@ class DefaultAsyncApiServiceTest { private DefaultAsyncApiService defaultAsyncApiService; private AsyncApiDocketService asyncApiDocketService; private ChannelsService channelsService; + private OperationsService operationsService; private SchemasService schemasService; private List customizers = new ArrayList<>(); @@ -33,13 +34,14 @@ class DefaultAsyncApiServiceTest { public void setup() { asyncApiDocketService = mock(AsyncApiDocketService.class); channelsService = mock(ChannelsService.class); + operationsService = mock(OperationsService.class); schemasService = mock(SchemasService.class); when(channelsService.findChannels()).thenReturn(Map.of()); when(schemasService.getSchemas()).thenReturn(Map.of()); - defaultAsyncApiService = - new DefaultAsyncApiService(asyncApiDocketService, channelsService, schemasService, customizers); + defaultAsyncApiService = new DefaultAsyncApiService( + asyncApiDocketService, channelsService, operationsService, schemasService, customizers); } @Test diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsServiceIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsServiceIntegrationTest.java index 73d3b721a..c559b5e17 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsServiceIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultChannelsServiceIntegrationTest.java @@ -39,36 +39,16 @@ void getChannels() { Map actualChannels = defaultChannelsService.findChannels(); assertThat(actualChannels) - .containsAllEntriesOf(simpleChannelScanner.scanChannels()) + .containsAllEntriesOf(simpleChannelScanner.scan()) .containsEntry(SameTopic.topicName, SameTopic.expectedMergedChannel); } - @Test - void getOperations() { - Map actualChannels = defaultChannelsService.findOperations(); - - assertThat(actualChannels) - .containsAllEntriesOf(simpleChannelScanner.scanOperations()) - .containsEntry("receive", SameTopic.ReceiveChannelScanner.receiveOperation) - .containsEntry("send", SameTopic.SendChannelScanner.sentOperation); - } - @Component static class SimpleChannelScanner implements ChannelsScanner { @Override - public Map scanChannels() { + public Map scan() { return Map.of("foo", new ChannelObject()); } - - @Override - public Map scanOperations() { - return Map.of( - "foo", - Operation.builder() - .channel(ChannelReference.fromChannel("foo")) - .action(OperationAction.RECEIVE) - .build()); - } } static class SameTopic { @@ -89,18 +69,13 @@ static class SendChannelScanner implements ChannelsScanner { .build(); @Override - public Map scanChannels() { + public Map scan() { return Map.of( topicName, ChannelObject.builder() .messages(Map.of("sendMessage", MessageReference.toComponentMessage("sendMessage"))) .build()); } - - @Override - public Map scanOperations() { - return Map.of("send", sentOperation); - } } @Component @@ -111,7 +86,7 @@ static class ReceiveChannelScanner implements ChannelsScanner { .build(); @Override - public Map scanChannels() { + public Map scan() { return Map.of( topicName, ChannelObject.builder() @@ -119,11 +94,6 @@ public Map scanChannels() { Map.of("receiveMessage", MessageReference.toComponentMessage("receiveMessage"))) .build()); } - - @Override - public Map scanOperations() { - return Map.of("receive", receiveOperation); - } } } } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultOperationsServiceIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultOperationsServiceIntegrationTest.java new file mode 100644 index 000000000..b82bb9727 --- /dev/null +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultOperationsServiceIntegrationTest.java @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi; + +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.OperationsScanner; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration( + classes = { + DefaultOperationsService.class, + DefaultOperationsServiceIntegrationTest.SimpleOperationScanner.class, + DefaultOperationsServiceIntegrationTest.SameTopic.ReceiveOperationScanner.class, + DefaultOperationsServiceIntegrationTest.SameTopic.SendOperationScanner.class + }) +class DefaultOperationsServiceIntegrationTest { + + @Autowired + private DefaultOperationsService defaultOperationsService; + + @Autowired + private SimpleOperationScanner simpleOperationsScanner; + + @Test + void getOperations() { + Map actualChannels = defaultOperationsService.findOperations(); + + assertThat(actualChannels) + .containsAllEntriesOf(simpleOperationsScanner.scan()) + .containsEntry("receive", SameTopic.ReceiveOperationScanner.receiveOperation) + .containsEntry("send", SameTopic.SendOperationScanner.sentOperation); + } + + @Component + static class SimpleOperationScanner implements OperationsScanner { + @Override + public Map scan() { + return Map.of( + "foo", + Operation.builder() + .channel(ChannelReference.fromChannel("foo")) + .action(OperationAction.RECEIVE) + .build()); + } + } + + static class SameTopic { + static final String topicName = "receiveSendTopic"; + + @Component + static class SendOperationScanner implements OperationsScanner { + static final Operation sentOperation = Operation.builder() + .channel(ChannelReference.fromChannel(topicName)) + .action(OperationAction.SEND) + .build(); + + @Override + public Map scan() { + return Map.of("send", sentOperation); + } + } + + @Component + static class ReceiveOperationScanner implements OperationsScanner { + static final Operation receiveOperation = Operation.builder() + .channel(ChannelReference.fromChannel(topicName)) + .action(OperationAction.RECEIVE) + .build(); + + @Override + public Map scan() { + return Map.of("receive", receiveOperation); + } + } + } +} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMergerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMergerTest.java index acdd148fb..d5d7e39f9 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMergerTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/ChannelMergerTest.java @@ -5,12 +5,9 @@ import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; import org.junit.jupiter.api.Test; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.Set; @@ -34,22 +31,6 @@ void shouldNotMergeDifferentChannelNames() { assertThat(mergedChannels).hasSize(2); } - @Test - void shouldNotMergeDifferentoperationIds() { - // given - String operationId1 = "operation1"; - String operationId2 = "operation2"; - Operation publisherOperation = Operation.builder().build(); - Operation subscriberOperation = Operation.builder().build(); - - // when - Map mergedOperations = ChannelMerger.mergeOperations(Arrays.asList( - Map.entry(operationId1, publisherOperation), Map.entry(operationId2, subscriberOperation))); - - // then - assertThat(mergedOperations).hasSize(2); - } - @Test void shouldMergeEqualChannelNamesIntoOneChannel() { // given @@ -65,27 +46,6 @@ void shouldMergeEqualChannelNamesIntoOneChannel() { assertThat(mergedChannels).hasSize(1); } - @Test - void shouldMergeEqualoperationIdsIntoOneOperation() { - // given - String operationId = "operation"; - Operation publishOperation = Operation.builder() - .action(OperationAction.SEND) - .title("publisher") - .build(); - Operation subscribeOperation = Operation.builder() - .action(OperationAction.RECEIVE) - .title("subscribe") - .build(); - - // when - Map mergedOperations = ChannelMerger.mergeOperations( - Arrays.asList(Map.entry(operationId, publishOperation), Map.entry(operationId, subscribeOperation))); - - // then - assertThat(mergedOperations).hasSize(1); - } - @Test void shouldUseFirstChannelFound() { // given @@ -105,25 +65,6 @@ void shouldUseFirstChannelFound() { }); } - @Test - void shouldUseFirstOperationFound() { - // given - String operationId = "operation"; - Operation senderOperation = - Operation.builder().action(OperationAction.SEND).build(); - Operation receiverOperation = - Operation.builder().action(OperationAction.RECEIVE).build(); - - // when - Map mergedOperations = ChannelMerger.mergeOperations( - Arrays.asList(Map.entry(operationId, senderOperation), Map.entry(operationId, receiverOperation))); - - // then - assertThat(mergedOperations).hasSize(1).hasEntrySatisfying(operationId, it -> { - assertThat(it.getAction()).isEqualTo(OperationAction.SEND); - }); - } - @Test void shouldMergeDifferentMessagesForSameChannel() { // given @@ -167,59 +108,6 @@ void shouldMergeDifferentMessagesForSameChannel() { }); } - @Test - void shouldMergeDifferentMessageForSameOperation() { - // given - String channelName = "channel"; - String operationId = "operation"; - MessageObject message1 = MessageObject.builder() - .messageId("message1") - .name(String.class.getCanonicalName()) - .description("This is a string") - .build(); - MessageObject message2 = MessageObject.builder() - .messageId("message2") - .name(Integer.class.getCanonicalName()) - .description("This is an integer") - .build(); - MessageObject message3 = MessageObject.builder() - .messageId("message3") - .name(Integer.class.getCanonicalName()) - .description("This is also an integer, but in essence the same payload type") - .build(); - MessageReference messageRef1 = MessageReference.toChannelMessage(channelName, message1); - MessageReference messageRef2 = MessageReference.toChannelMessage(channelName, message2); - MessageReference messageRef3 = MessageReference.toChannelMessage(channelName, message3); - - Operation senderOperation1 = Operation.builder() - .action(OperationAction.SEND) - .title("sender1") - .messages(List.of(messageRef1)) - .build(); - Operation senderOperation2 = Operation.builder() - .action(OperationAction.SEND) - .title("sender2") - .messages(List.of(messageRef2)) - .build(); - Operation senderOperation3 = Operation.builder() - .action(OperationAction.SEND) - .title("sender3") - .messages(List.of(messageRef3)) - .build(); - - // when - Map mergedOperations = ChannelMerger.mergeOperations(List.of( - Map.entry(operationId, senderOperation1), - Map.entry(operationId, senderOperation2), - Map.entry(operationId, senderOperation3))); - - // then expectedMessage only includes message1 and message2. - // Message3 is not included as it is identical in terms of payload type (Message#name) to message 2 - assertThat(mergedOperations).hasSize(1).hasEntrySatisfying(operationId, it -> { - assertThat(it.getMessages()).containsExactlyInAnyOrder(messageRef1, messageRef2); - }); - } - @Test void shouldUseOtherMessageIfFirstMessageIsMissingForChannels() { // given @@ -246,35 +134,4 @@ void shouldUseOtherMessageIfFirstMessageIsMissingForChannels() { assertThat(it.getMessages()).containsExactlyInAnyOrderEntriesOf(expectedMessages); }); } - - @Test - void shouldUseOtherMessageIfFirstMessageIsMissingForOperations() { - // given - String channelName = "channel-name"; - MessageObject message2 = MessageObject.builder() - .messageId(String.class.getCanonicalName()) - .name(String.class.getCanonicalName()) - .description("This is a string") - .build(); - Operation publishOperation1 = Operation.builder() - .action(OperationAction.SEND) - .title("publisher1") - .build(); - Operation publishOperation2 = Operation.builder() - .action(OperationAction.SEND) - .title("publisher2") - .messages(List.of(MessageReference.toChannelMessage(channelName, message2))) - .build(); - - // when - Map mergedOperations = ChannelMerger.mergeOperations( - Arrays.asList(Map.entry("publisher1", publishOperation1), Map.entry("publisher1", publishOperation2))); - // then expectedMessage message2 - var expectedMessage = MessageReference.toChannelMessage(channelName, message2); - - assertThat(mergedOperations).hasSize(1).hasEntrySatisfying("publisher1", it -> { - assertThat(it.getMessages()).hasSize(1); - assertThat(it.getMessages()).containsExactlyInAnyOrder(expectedMessage); - }); - } } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationMergerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationMergerTest.java new file mode 100644 index 000000000..75b8d6a82 --- /dev/null +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/OperationMergerTest.java @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels; + +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class OperationMergerTest { + + @Test + void shouldNotMergeDifferentoperationIds() { + // given + String operationId1 = "operation1"; + String operationId2 = "operation2"; + Operation publisherOperation = Operation.builder().build(); + Operation subscriberOperation = Operation.builder().build(); + + // when + Map mergedOperations = OperationMerger.mergeOperations(Arrays.asList( + Map.entry(operationId1, publisherOperation), Map.entry(operationId2, subscriberOperation))); + + // then + assertThat(mergedOperations).hasSize(2); + } + + @Test + void shouldMergeEqualoperationIdsIntoOneOperation() { + // given + String operationId = "operation"; + Operation publishOperation = Operation.builder() + .action(OperationAction.SEND) + .title("publisher") + .build(); + Operation subscribeOperation = Operation.builder() + .action(OperationAction.RECEIVE) + .title("subscribe") + .build(); + + // when + Map mergedOperations = OperationMerger.mergeOperations( + Arrays.asList(Map.entry(operationId, publishOperation), Map.entry(operationId, subscribeOperation))); + + // then + assertThat(mergedOperations).hasSize(1); + } + + @Test + void shouldUseFirstOperationFound() { + // given + String operationId = "operation"; + Operation senderOperation = + Operation.builder().action(OperationAction.SEND).build(); + Operation receiverOperation = + Operation.builder().action(OperationAction.RECEIVE).build(); + + // when + Map mergedOperations = OperationMerger.mergeOperations( + Arrays.asList(Map.entry(operationId, senderOperation), Map.entry(operationId, receiverOperation))); + + // then + assertThat(mergedOperations).hasSize(1).hasEntrySatisfying(operationId, it -> { + assertThat(it.getAction()).isEqualTo(OperationAction.SEND); + }); + } + + @Test + void shouldMergeDifferentMessageForSameOperation() { + // given + String channelName = "channel"; + String operationId = "operation"; + MessageObject message1 = MessageObject.builder() + .messageId("message1") + .name(String.class.getCanonicalName()) + .description("This is a string") + .build(); + MessageObject message2 = MessageObject.builder() + .messageId("message2") + .name(Integer.class.getCanonicalName()) + .description("This is an integer") + .build(); + MessageObject message3 = MessageObject.builder() + .messageId("message3") + .name(Integer.class.getCanonicalName()) + .description("This is also an integer, but in essence the same payload type") + .build(); + MessageReference messageRef1 = MessageReference.toChannelMessage(channelName, message1); + MessageReference messageRef2 = MessageReference.toChannelMessage(channelName, message2); + MessageReference messageRef3 = MessageReference.toChannelMessage(channelName, message3); + + Operation senderOperation1 = Operation.builder() + .action(OperationAction.SEND) + .title("sender1") + .messages(List.of(messageRef1)) + .build(); + Operation senderOperation2 = Operation.builder() + .action(OperationAction.SEND) + .title("sender2") + .messages(List.of(messageRef2)) + .build(); + Operation senderOperation3 = Operation.builder() + .action(OperationAction.SEND) + .title("sender3") + .messages(List.of(messageRef3)) + .build(); + + // when + Map mergedOperations = OperationMerger.mergeOperations(List.of( + Map.entry(operationId, senderOperation1), + Map.entry(operationId, senderOperation2), + Map.entry(operationId, senderOperation3))); + + // then expectedMessage only includes message1 and message2. + // Message3 is not included as it is identical in terms of payload type (Message#name) to message 2 + assertThat(mergedOperations).hasSize(1).hasEntrySatisfying(operationId, it -> { + assertThat(it.getMessages()).containsExactlyInAnyOrder(messageRef1, messageRef2); + }); + } + + @Test + void shouldUseOtherMessageIfFirstMessageIsMissingForChannels() { + // given + String channelName = "channel"; + MessageObject message2 = MessageObject.builder() + .messageId(String.class.getCanonicalName()) + .name(String.class.getCanonicalName()) + .description("This is a string") + .build(); + ChannelObject publisherChannel1 = ChannelObject.builder().build(); + ChannelObject publisherChannel2 = ChannelObject.builder() + .messages(Map.of(message2.getName(), message2)) + .build(); + + // when + Map mergedChannels = ChannelMerger.mergeChannels( + Arrays.asList(Map.entry(channelName, publisherChannel1), Map.entry(channelName, publisherChannel2))); + + // then expectedMessage message2 + var expectedMessages = Map.of(message2.getName(), message2); + + assertThat(mergedChannels).hasSize(1).hasEntrySatisfying(channelName, it -> { + assertThat(it.getMessages()).hasSize(1); + assertThat(it.getMessages()).containsExactlyInAnyOrderEntriesOf(expectedMessages); + }); + } + + @Test + void shouldUseOtherMessageIfFirstMessageIsMissingForOperations() { + // given + String channelName = "channel-name"; + MessageObject message2 = MessageObject.builder() + .messageId(String.class.getCanonicalName()) + .name(String.class.getCanonicalName()) + .description("This is a string") + .build(); + Operation publishOperation1 = Operation.builder() + .action(OperationAction.SEND) + .title("publisher1") + .build(); + Operation publishOperation2 = Operation.builder() + .action(OperationAction.SEND) + .title("publisher2") + .messages(List.of(MessageReference.toChannelMessage(channelName, message2))) + .build(); + + // when + Map mergedOperations = OperationMerger.mergeOperations( + Arrays.asList(Map.entry("publisher1", publishOperation1), Map.entry("publisher1", publishOperation2))); + // then expectedMessage message2 + var expectedMessage = MessageReference.toChannelMessage(channelName, message2); + + assertThat(mergedOperations).hasSize(1).hasEntrySatisfying("publisher1", it -> { + assertThat(it.getMessages()).hasSize(1); + assertThat(it.getMessages()).containsExactlyInAnyOrder(expectedMessage); + }); + } +} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScannerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScannerTest.java index f00ed12ef..9c0e8c365 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScannerTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleChannelsScannerTest.java @@ -3,7 +3,6 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; import org.junit.jupiter.api.Test; import java.util.Map; @@ -26,12 +25,10 @@ class SimpleChannelsScannerTest { @Test void noClassFoundTest() { // when - Map channels = simpleChannelsScanner.scanChannels(); - Map operations = simpleChannelsScanner.scanOperations(); + Map channels = simpleChannelsScanner.scan(); // then assertThat(channels).isEmpty(); - assertThat(operations).isEmpty(); } @Test @@ -42,10 +39,10 @@ void processClassTest() { Map.entry("channel1", ChannelObject.builder().build()); Map.Entry channel2 = Map.entry("channel2", ChannelObject.builder().build()); - when(classProcessor.processChannels(any())).thenReturn(Stream.of(channel1, channel2)); + when(classProcessor.process(any())).thenReturn(Stream.of(channel1, channel2)); // when - Map channels = simpleChannelsScanner.scanChannels(); + Map channels = simpleChannelsScanner.scan(); // then assertThat(channels).containsExactly(channel1, channel2); @@ -59,10 +56,10 @@ void sameChannelsAreMergedTest() { Map.entry("channel1", ChannelObject.builder().build()); Map.Entry channel2 = Map.entry("channel1", ChannelObject.builder().build()); - when(classProcessor.processChannels(any())).thenReturn(Stream.of(channel1, channel2)); + when(classProcessor.process(any())).thenReturn(Stream.of(channel1, channel2)); // when - Map channels = simpleChannelsScanner.scanChannels(); + Map channels = simpleChannelsScanner.scan(); // then assertThat(channels) @@ -73,10 +70,10 @@ void sameChannelsAreMergedTest() { void processEmptyClassTest() { // given when(classScanner.scan()).thenReturn(Set.of(String.class)); - when(classProcessor.processChannels(any())).thenReturn(Stream.of()); + when(classProcessor.process(any())).thenReturn(Stream.of()); // when - Map channels = simpleChannelsScanner.scanChannels(); + Map channels = simpleChannelsScanner.scan(); // then assertThat(channels).isEmpty(); diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleOperationsScannerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleOperationsScannerTest.java new file mode 100644 index 000000000..770c92fce --- /dev/null +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/SimpleOperationsScannerTest.java @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels; + +import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ClassScanner; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SimpleOperationsScannerTest { + + private final ClassScanner classScanner = mock(ClassScanner.class); + private final SimpleOperationsScanner.ClassProcessor classProcessor = + mock(SimpleOperationsScanner.ClassProcessor.class); + + private final SimpleOperationsScanner simpleOperationsScanner = + new SimpleOperationsScanner(classScanner, classProcessor); + + @Test + void noClassFoundTest() { + // when + Map operations = simpleOperationsScanner.scan(); + + // then + assertThat(operations).isEmpty(); + } + + @Test + void processClassTest() { + // given + when(classScanner.scan()).thenReturn(Set.of(String.class)); + Map.Entry operation1 = + Map.entry("operation1", Operation.builder().build()); + Map.Entry operation2 = + Map.entry("operation2", Operation.builder().build()); + when(classProcessor.process(any())).thenReturn(Stream.of(operation1, operation2)); + + // when + Map operations = simpleOperationsScanner.scan(); + + // then + assertThat(operations).containsExactly(operation2, operation1); + } + + @Test + void sameOperationsAreMergedTest() { + // given + when(classScanner.scan()).thenReturn(Set.of(String.class)); + Map.Entry operation1 = + Map.entry("operation1", Operation.builder().build()); + Map.Entry operation2 = + Map.entry("operation1", Operation.builder().build()); + when(classProcessor.process(any())).thenReturn(Stream.of(operation1, operation2)); + + // when + Map operations = simpleOperationsScanner.scan(); + + // then + assertThat(operations) + .containsExactly(Map.entry("operation1", Operation.builder().build())); + } + + @Test + void processEmptyClassTest() { + // given + when(classScanner.scan()).thenReturn(Set.of(String.class)); + when(classProcessor.process(any())).thenReturn(Stream.of()); + + // when + Map operations = simpleOperationsScanner.scan(); + + // then + assertThat(operations).isEmpty(); + } +} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScannerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScannerTest.java index 57bca30e3..325903449 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScannerTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationChannelsScannerTest.java @@ -58,8 +58,8 @@ class AsyncAnnotationChannelsScannerTest { - private AsyncAnnotationChannelsScanner.AsyncAnnotationProvider asyncAnnotationProvider = - new AsyncAnnotationChannelsScanner.AsyncAnnotationProvider<>() { + private final AsyncAnnotationScanner.AsyncAnnotationProvider asyncAnnotationProvider = + new AsyncAnnotationScanner.AsyncAnnotationProvider<>() { @Override public Class getAnnotation() { return AsyncListener.class; @@ -125,7 +125,7 @@ private void setClassToScan(Class classToScan) { void scan_componentHasNoListenerMethods() { setClassToScan(ClassWithoutListenerAnnotation.class); - Map channels = channelScanner.scanChannels(); + Map channels = channelScanner.scan(); assertThat(channels).isEmpty(); } @@ -136,7 +136,7 @@ void scan_componentChannelHasListenerMethod() { setClassToScan(ClassWithListenerAnnotation.class); // When scan is called - Map actualChannels = channelScanner.scanChannels(); + Map actualChannels = channelScanner.scan(); // Then the returned collection contains the channel MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() @@ -166,7 +166,7 @@ void scan_componentHasListenerMethodWithUnknownServer() { // Given a class with method annotated with AsyncListener, with an unknown servername setClassToScan(ClassWithListenerAnnotationWithInvalidServer.class); - assertThatThrownBy(channelScanner::scanChannels) + assertThatThrownBy(channelScanner::scan) .isInstanceOf(IllegalArgumentException.class) .hasMessage( "Operation 'test-channel_send' defines unknown server ref 'server3'. This AsyncApi defines these server(s): [server1, server2]"); @@ -178,8 +178,7 @@ void scan_componentHasListenerMethodWithAllAttributes() { setClassToScan(ClassWithListenerAnnotationWithAllAttributes.class); // When scan is called - Map actualChannels = channelScanner.scanChannels(); - Map actualOperations = channelScanner.scanOperations(); + Map actualChannels = channelScanner.scan(); // Then the returned collection contains the channel MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() @@ -214,8 +213,6 @@ void scan_componentHasListenerMethodWithAllAttributes() { .build(); assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - assertThat(actualOperations) - .containsExactly(Map.entry("test-channel_send_methodWithAnnotation", expectedOperation)); } @Test @@ -224,8 +221,7 @@ void scan_componentHasMultipleListenerAnnotations() { setClassToScan(ClassWithMultipleListenerAnnotations.class); // When scan is called - Map actualChannels = channelScanner.scanChannels(); - Map actualOperations = channelScanner.scanOperations(); + Map actualChannels = channelScanner.scan(); // Then the returned collection contains the channel MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() @@ -253,7 +249,7 @@ void scan_componentHasMultipleListenerAnnotations() { ChannelObject expectedChannel1 = ChannelObject.builder() .messages(Map.of(message.getMessageId(), MessageReference.toComponentMessage(message))) - .bindings(null) /*.publish(operation1)*/ + .bindings(null) .build(); Operation expectedOperation2 = Operation.builder() @@ -267,17 +263,13 @@ void scan_componentHasMultipleListenerAnnotations() { ChannelObject expectedChannel2 = ChannelObject.builder() .messages(Map.of(message.getMessageId(), MessageReference.toComponentMessage(message))) - .bindings(null) /*.publish(operation2)*/ + .bindings(null) .build(); assertThat(actualChannels) .containsExactlyInAnyOrderEntriesOf(Map.of( "test-channel-1", expectedChannel1, "test-channel-2", expectedChannel2)); - assertThat(actualOperations) - .containsExactlyInAnyOrderEntriesOf(Map.of( - "test-channel-1_send_methodWithMultipleAnnotation", expectedOperation1, - "test-channel-2_send_methodWithMultipleAnnotation", expectedOperation2)); } @Test @@ -286,8 +278,7 @@ void scan_componentHasAsyncMethodAnnotation() { setClassToScan(ClassWithMessageAnnotation.class); // When scan is called - Map actualChannels = channelScanner.scanChannels(); - Map actualOperations = channelScanner.scanOperations(); + Map actualChannels = channelScanner.scan(); // Then the returned collection contains the channel MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() @@ -319,8 +310,6 @@ void scan_componentHasAsyncMethodAnnotation() { .build(); assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - assertThat(actualOperations) - .containsExactly(Map.entry("test-channel_send_methodWithAnnotation", expectedOperation)); } private static class ClassWithoutListenerAnnotation { @@ -405,8 +394,7 @@ void scan_componentHasOnlyDeclaredMethods(Class clazz) { setClassToScan(clazz); // When scan is called - Map actualChannels = channelScanner.scanChannels(); - Map actualOperations = channelScanner.scanOperations(); + Map actualChannels = channelScanner.scan(); // Then the returned collection contains the channel with the actual method, excluding type erased methods var messagePayload = MessagePayload.of(MultiFormatSchema.builder() @@ -437,8 +425,6 @@ void scan_componentHasOnlyDeclaredMethods(Class clazz) { .build(); assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - assertThat(actualOperations) - .containsExactly(Map.entry("test-channel_send_methodFromInterface", expectedOperation)); } private static class ClassImplementingInterface implements ClassInterface { @@ -480,8 +466,7 @@ void scan_componentHasListenerMethodWithMetaAnnotation() { setClassToScan(ClassWithMetaAnnotation.class); // When scan is called - Map actualChannels = channelScanner.scanChannels(); - Map actualOperations = channelScanner.scanOperations(); + Map actualChannels = channelScanner.scan(); // Then the returned collection contains the channel var messagePayload = MessagePayload.of(MultiFormatSchema.builder() @@ -513,8 +498,6 @@ void scan_componentHasListenerMethodWithMetaAnnotation() { .build(); assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); - assertThat(actualOperations) - .containsExactly(Map.entry("test-channel_send_methodFromInterface", expectedOperation)); } public static class ClassWithMetaAnnotation { diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationOperationsScannerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationOperationsScannerTest.java new file mode 100644 index 000000000..8ed34f92c --- /dev/null +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AsyncAnnotationOperationsScannerTest.java @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +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.channel.operation.message.header.AsyncHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ServerReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.info.Info; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.server.Server; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocket; +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.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.util.StringValueResolver; + +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.List; +import java.util.Map; +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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AsyncAnnotationOperationsScannerTest { + + private final AsyncAnnotationScanner.AsyncAnnotationProvider asyncAnnotationProvider = + new AsyncAnnotationScanner.AsyncAnnotationProvider<>() { + @Override + public Class getAnnotation() { + return AsyncListener.class; + } + + @Override + public AsyncOperation getAsyncOperation(AsyncListener annotation) { + return annotation.operation(); + } + + @Override + public OperationAction getOperationType() { + return OperationAction.SEND; + } + }; + private final SpringwolfConfigProperties properties = new SpringwolfConfigProperties(); + private final ClassScanner classScanner = mock(ClassScanner.class); + private final SchemasService schemasService = new DefaultSchemasService(emptyList(), emptyList(), 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 AsyncAnnotationOperationsScanner operationsScanner = + new AsyncAnnotationOperationsScanner<>( + asyncAnnotationProvider, + classScanner, + schemasService, + payloadClassExtractor, + operationBindingProcessors, + messageBindingProcessors); + + @BeforeEach + public void setup() { + when(asyncApiDocketService.getAsyncApiDocket()) + .thenReturn(AsyncApiDocket.builder() + .info(new Info()) + .server("server1", new Server()) + .server("server2", new Server()) + .build()); + + operationsScanner.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 + void scan_componentHasNoListenerMethods() { + setClassToScan(ClassWithoutListenerAnnotation.class); + + Map channels = operationsScanner.scan(); + + assertThat(channels).isEmpty(); + } + + @Test + void scan_componentOperationHasListenerMethod() { + // Given a class with methods annotated with AsyncListener, where only the channel-name is set + setClassToScan(ClassWithListenerAnnotation.class); + + // When scan is called + Map actualOperations = operationsScanner.scan(); + + // Then the returned collection contains the channel + MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(SimpleFoo.class.getSimpleName())) + .build()); + + MessageObject message = MessageObject.builder() + .messageId(SimpleFoo.class.getName()) + .name(SimpleFoo.class.getName()) + .title(SimpleFoo.class.getSimpleName()) + .description("SimpleFoo Message Description") + .payload(payload) + .headers(MessageHeaders.of(MessageReference.toSchema(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))) + .bindings(EMPTY_MAP) + .build(); + + Operation expectedOperation = Operation.builder() + .title("test-channel_send") + .action(OperationAction.SEND) + .description("Auto-generated description") + .channel(ChannelReference.fromChannel("test-channel")) + .messages(List.of(MessageReference.toChannelMessage("test-channel", message))) + .bindings(EMPTY_MAP) + .build(); + + assertThat(actualOperations) + .containsExactly(Map.entry("test-channel_send_methodWithAnnotation", expectedOperation)); + } + + @Test + void scan_componentHasListenerMethodWithAllAttributes() { + // Given a class with method annotated with AsyncListener, where all attributes are set + setClassToScan(ClassWithListenerAnnotationWithAllAttributes.class); + + // When scan is called + Map actualOperations = operationsScanner.scan(); + + // Then the returned collection contains the channel + MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(String.class.getSimpleName())) + .build()); + + MessageObject message = MessageObject.builder() + .messageId(String.class.getName()) + .name(String.class.getName()) + .title(String.class.getSimpleName()) + .description(null) + .payload(payload) + .headers(MessageHeaders.of(MessageReference.toSchema("TestSchema"))) + .bindings(EMPTY_MAP) + .build(); + + ChannelObject expectedChannel = ChannelObject.builder() + .bindings(null) + .messages(Map.of(message.getMessageId(), MessageReference.toComponentMessage(message))) + .servers(List.of( + ServerReference.builder().ref("server1").build(), + ServerReference.builder().ref("server2").build())) + .build(); + + Operation expectedOperation = Operation.builder() + .action(OperationAction.SEND) + .title("test-channel_send") + .channel(ChannelReference.fromChannel("test-channel")) + .description("description") + .bindings(Map.of(TestOperationBindingProcessor.TYPE, TestOperationBindingProcessor.BINDING)) + .messages(List.of(MessageReference.toChannelMessage("test-channel", message))) + .build(); + + assertThat(actualOperations) + .containsExactly(Map.entry("test-channel_send_methodWithAnnotation", expectedOperation)); + } + + @Test + void scan_componentHasMultipleListenerAnnotations() { + // Given a class with methods annotated with AsyncListener, where only the channel-name is set + setClassToScan(ClassWithMultipleListenerAnnotations.class); + + // When scan is called + Map actualOperations = operationsScanner.scan(); + + // Then the returned collection contains the channel + MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(SimpleFoo.class.getSimpleName())) + .build()); + + MessageObject message = MessageObject.builder() + .messageId(SimpleFoo.class.getName()) + .name(SimpleFoo.class.getName()) + .title(SimpleFoo.class.getSimpleName()) + .payload(payload) + .headers(MessageHeaders.of(MessageReference.toSchema(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))) + .bindings(EMPTY_MAP) + .description("SimpleFoo Message Description") + .build(); + + Operation expectedOperation1 = Operation.builder() + .action(OperationAction.SEND) + .channel(ChannelReference.fromChannel("test-channel-1")) + .description("test-channel-1-description") + .title("test-channel-1_send") + .bindings(EMPTY_MAP) + .messages(List.of(MessageReference.toChannelMessage("test-channel-1", message))) + .build(); + + ChannelObject expectedChannel1 = ChannelObject.builder() + .messages(Map.of(message.getMessageId(), MessageReference.toComponentMessage(message))) + .bindings(null) + .build(); + + Operation expectedOperation2 = Operation.builder() + .action(OperationAction.SEND) + .channel(ChannelReference.fromChannel("test-channel-2")) + .description("test-channel-2-description") + .title("test-channel-2_send") + .bindings(EMPTY_MAP) + .messages(List.of(MessageReference.toChannelMessage("test-channel-2", message))) + .build(); + + ChannelObject expectedChannel2 = ChannelObject.builder() + .messages(Map.of(message.getMessageId(), MessageReference.toComponentMessage(message))) + .bindings(null) + .build(); + + assertThat(actualOperations) + .containsExactlyInAnyOrderEntriesOf(Map.of( + "test-channel-1_send_methodWithMultipleAnnotation", expectedOperation1, + "test-channel-2_send_methodWithMultipleAnnotation", expectedOperation2)); + } + + @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 actualOperations = operationsScanner.scan(); + + // Then the returned collection contains the channel + MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(SimpleFoo.class.getSimpleName())) + .build()); + + MessageObject message = MessageObject.builder() + .messageId("simpleFoo") + .name("SimpleFooPayLoad") + .title("Message Title") + .description("Message description") + .payload(payload) + .headers(MessageHeaders.of(MessageReference.toSchema(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))) + .bindings(EMPTY_MAP) + .build(); + + Operation expectedOperation = Operation.builder() + .action(OperationAction.SEND) + .channel(ChannelReference.fromChannel("test-channel")) + .description("test channel operation description") + .title("test-channel_send") + .bindings(EMPTY_MAP) + .messages(List.of(MessageReference.toChannelMessage("test-channel", message))) + .build(); + + ChannelObject expectedChannel = ChannelObject.builder() + .bindings(null) + .messages(Map.of(message.getName(), MessageReference.toComponentMessage(message))) + .build(); + + assertThat(actualOperations) + .containsExactly(Map.entry("test-channel_send_methodWithAnnotation", expectedOperation)); + } + + private static class ClassWithoutListenerAnnotation { + + private void methodWithoutAnnotation() {} + } + + private static class ClassWithListenerAnnotation { + + @AsyncListener(operation = @AsyncOperation(channelName = "test-channel")) + private void methodWithAnnotation(SimpleFoo payload) {} + + private void methodWithoutAnnotation() {} + } + + private static class ClassWithListenerAnnotationWithInvalidServer { + + @AsyncListener( + operation = + @AsyncOperation( + channelName = "test-channel", + description = "test channel operation description", + servers = {"server3"})) + private void methodWithAnnotation(SimpleFoo payload) {} + } + + private static class ClassWithListenerAnnotationWithAllAttributes { + + @AsyncListener( + operation = + @AsyncOperation( + channelName = "${test.property.test-channel}", + description = "${test.property.description}", + payloadType = String.class, + servers = {"${test.property.server1}", "${test.property.server2}"}, + 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 { + + @AsyncListener( + operation = @AsyncOperation(channelName = "test-channel-1", description = "test-channel-1-description")) + @AsyncListener( + operation = @AsyncOperation(channelName = "test-channel-2", description = "test-channel-2-description")) + private void methodWithMultipleAnnotation(SimpleFoo payload) {} + } + + private static class ClassWithMessageAnnotation { + + @AsyncListener( + operation = + @AsyncOperation( + channelName = "test-channel", + description = "test channel operation description", + message = + @AsyncMessage( + description = "Message description", + messageId = "simpleFoo", + name = "SimpleFooPayLoad", + contentType = "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 actualOperations = operationsScanner.scan(); + + // Then the returned collection contains the channel with the actual method, excluding type erased methods + var messagePayload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(String.class.getSimpleName())) + .build()); + MessageObject message = MessageObject.builder() + .messageId(String.class.getName()) + .name(String.class.getName()) + .title(String.class.getSimpleName()) + .description(null) + .payload(messagePayload) + .headers(MessageHeaders.of(MessageReference.toSchema(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))) + .bindings(EMPTY_MAP) + .build(); + + Operation expectedOperation = Operation.builder() + .action(OperationAction.SEND) + .channel(ChannelReference.fromChannel("test-channel")) + .description("test channel operation description") + .title("test-channel_send") + .bindings(EMPTY_MAP) + .messages(List.of(MessageReference.toChannelMessage("test-channel", message))) + .build(); + + ChannelObject expectedChannel = ChannelObject.builder() + .bindings(null) + .messages(Map.of(message.getMessageId(), MessageReference.toComponentMessage(message))) + .build(); + + assertThat(actualOperations) + .containsExactly(Map.entry("test-channel_send_methodFromInterface", expectedOperation)); + } + + private static class ClassImplementingInterface implements ClassInterface { + + @AsyncListener( + 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 { + @AsyncListener( + operation = + @AsyncOperation( + channelName = "test-channel", + description = "test channel operation description")) + void methodFromInterface(T payload); + } + } + + @Nested + class MetaAnnotation { + @Test + void scan_componentHasListenerMethodWithMetaAnnotation() { + // Given a class with methods annotated with a AsyncListener meta annotation + setClassToScan(ClassWithMetaAnnotation.class); + + // When scan is called + Map actualOperations = operationsScanner.scan(); + + // Then the returned collection contains the channel + var messagePayload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(String.class.getSimpleName())) + .build()); + + MessageObject message = MessageObject.builder() + .messageId(String.class.getName()) + .name(String.class.getName()) + .title(String.class.getSimpleName()) + .description(null) + .payload(messagePayload) + .headers(MessageHeaders.of(MessageReference.toSchema(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))) + .bindings(EMPTY_MAP) + .build(); + + Operation expectedOperation = Operation.builder() + .action(OperationAction.SEND) + .channel(ChannelReference.fromChannel("test-channel")) + .description("test channel operation description") + .title("test-channel_send") + .bindings(EMPTY_MAP) + .messages(List.of(MessageReference.toChannelMessage("test-channel", message))) + .build(); + + ChannelObject expectedChannel = ChannelObject.builder() + .bindings(null) + .messages(Map.of(message.getMessageId(), MessageReference.toComponentMessage(message))) + .build(); + + assertThat(actualOperations) + .containsExactly(Map.entry("test-channel_send_methodFromInterface", expectedOperation)); + } + + public static class ClassWithMetaAnnotation { + @AsyncListenerMetaAnnotation + void methodFromInterface(String payload) {} + } + + @Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @AsyncListener( + operation = + @AsyncOperation( + channelName = "test-channel", + description = "test channel operation description")) + public @interface AsyncListenerMetaAnnotation {} + } + + @Data + @NoArgsConstructor + @Schema(description = "SimpleFoo Message Description") + private static class SimpleFoo { + private String s; + private boolean b; + } +} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerIntegrationTest.java index a47e1287d..95885078d 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerIntegrationTest.java @@ -78,7 +78,7 @@ class NoClassListener { void scan_componentHasNoClassLevelRabbitListenerAnnotation() { // when List> channels = - scanner.processChannels(ClassWithoutClassListener.class).toList(); + scanner.process(ClassWithoutClassListener.class).toList(); // then assertThat(channels).isEmpty(); @@ -97,7 +97,7 @@ class NoMethodListener { void scan_componentHasNoClassLevelRabbitListenerAnnotation() { // when List> channels = - scanner.processChannels(ClassWithoutMethodListener.class).toList(); + scanner.process(ClassWithoutMethodListener.class).toList(); // then assertThat(channels).isEmpty(); @@ -115,9 +115,8 @@ class OneMethodLevelAnnotation { @Test void scan_componentWithOneMethodLevelAnnotation() { // when - List> actualChannels = scanner.processChannels( - ClassWithOneMethodLevelHandler.class) - .toList(); + List> actualChannels = + scanner.process(ClassWithOneMethodLevelHandler.class).toList(); // then MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() @@ -158,9 +157,8 @@ class MultipleMethodLevelAnnotations { @Test void scan_componentWithMultipleRabbitHandlerMethods() { // when - List> actualChannels = scanner.processChannels( - ClassWithMultipleMethodLevelHandlers.class) - .toList(); + List> actualChannels = + scanner.process(ClassWithMultipleMethodLevelHandlers.class).toList(); // Then the returned collection contains the channel with message set to oneOf MessagePayload simpleFooPayload = MessagePayload.of(MultiFormatSchema.builder() diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerTest.java index 9855fb15f..8a66b459d 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationChannelsScannerTest.java @@ -28,7 +28,6 @@ import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -81,7 +80,7 @@ void setUp() { void scan_componentHasTestListenerMethods() { // when List> channels = - scanner.processChannels(ClassWithTestListenerAnnotation.class).collect(Collectors.toList()); + scanner.process(ClassWithTestListenerAnnotation.class).toList(); // then MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationOperationsScannerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationOperationsScannerTest.java new file mode 100644 index 000000000..d79837b5f --- /dev/null +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelAnnotationOperationsScannerTest.java @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.BindingFactory; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersNotDocumented; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.ChannelBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.amqp.AMQPMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.amqp.AMQPOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; +import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ClassLevelAnnotationOperationsScannerTest { + + private final PayloadClassExtractor payloadClassExtractor = mock(PayloadClassExtractor.class); + private final BindingFactory bindingFactory = mock(BindingFactory.class); + private final SchemasService schemasService = mock(SchemasService.class); + ClassLevelAnnotationOperationsScanner scanner = + new ClassLevelAnnotationOperationsScanner<>( + TestClassListener.class, + TestMethodListener.class, + bindingFactory, + new AsyncHeadersNotDocumented(), + payloadClassExtractor, + schemasService); + + private static final String CHANNEL = "test-channel"; + private static final Map defaultOperationBinding = + Map.of("protocol", new AMQPOperationBinding()); + private static final Map defaultMessageBinding = + Map.of("protocol", new AMQPMessageBinding()); + private static final Map defaultChannelBinding = + Map.of("protocol", new AMQPChannelBinding()); + + @BeforeEach + void setUp() { + // when + when(bindingFactory.getChannelName(any())).thenReturn(CHANNEL); + + doReturn(defaultOperationBinding).when(bindingFactory).buildOperationBinding(any()); + doReturn(defaultChannelBinding).when(bindingFactory).buildChannelBinding(any()); + doReturn(defaultMessageBinding).when(bindingFactory).buildMessageBinding(any()); + + doReturn(String.class).when(payloadClassExtractor).extractFrom(any()); + doAnswer(invocation -> invocation.>getArgument(0).getSimpleName()) + .when(schemasService) + .registerSchema(any(Class.class)); + doAnswer(invocation -> AsyncHeaders.NOT_DOCUMENTED.getSchemaName()) + .when(schemasService) + .registerSchema(any(AsyncHeaders.class)); + } + + @Test + void scan_componentHasTestListenerMethods() { + // when + List> operations = + scanner.process(ClassWithTestListenerAnnotation.class).toList(); + + // then + MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() + .schema(SchemaReference.fromSchema(String.class.getSimpleName())) + .build()); + + MessageObject message = MessageObject.builder() + .messageId(String.class.getName()) + .name(String.class.getName()) + .title(String.class.getSimpleName()) + .payload(payload) + .headers(MessageHeaders.of(MessageReference.toSchema(AsyncHeaders.NOT_DOCUMENTED.getSchemaName()))) + .bindings(defaultMessageBinding) + .build(); + + Operation expectedOperation = Operation.builder() + .action(OperationAction.RECEIVE) + .channel(ChannelReference.fromChannel(CHANNEL)) + .messages(List.of(MessageReference.toChannelMessage(CHANNEL, message))) + .bindings(Map.of("protocol", AMQPOperationBinding.builder().build())) + .build(); + String operationName = CHANNEL + "_receive_ClassWithTestListenerAnnotation"; + assertThat(operations).containsExactly(Map.entry(operationName, expectedOperation)); + } + + @TestClassListener + private static class ClassWithTestListenerAnnotation { + @TestMethodListener + private void methodWithAnnotation(String payload) {} + + private void methodWithoutAnnotation() {} + } + + @Data + @NoArgsConstructor + private static class SimpleFoo { + private String s; + private boolean b; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestClassListener {} + + @Retention(RetentionPolicy.RUNTIME) + @interface TestMethodListener {} +} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerIntegrationTest.java index b52917ac8..a1b12ed33 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerIntegrationTest.java @@ -73,9 +73,8 @@ class NoListener { @Test void scan_componentHasNoListenerMethods() { // when - List> channels = scanner.processChannels( - ClassWithoutListenerAnnotation.class) - .toList(); + List> channels = + scanner.process(ClassWithoutListenerAnnotation.class).toList(); // then assertThat(channels).isEmpty(); @@ -92,7 +91,7 @@ class WithListener { void scan_componentHasListenerMethod() { // when List> actualChannels = - scanner.processChannels(ClassWithListenerAnnotation.class).toList(); + scanner.process(ClassWithListenerAnnotation.class).toList(); // then MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() @@ -130,7 +129,7 @@ class OneChannelTwoPayloads { @Test void scan_componentHasTestListenerMethods_multiplePayloads() { // when - List> channels = scanner.processChannels( + List> channels = scanner.process( ClassWithTestListenerAnnotationMultiplePayloads.class) .toList(); @@ -191,9 +190,8 @@ class MetaAnnotation { @Test void scan_componentHasListenerMetaMethod() { // when - List> actualChannels = scanner.processChannels( - ClassWithListenerMetaAnnotation.class) - .toList(); + List> actualChannels = + scanner.process(ClassWithListenerMetaAnnotation.class).toList(); // then MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerTest.java index bf840e093..860479a00 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelAnnotationChannelsScannerTest.java @@ -82,7 +82,7 @@ void setUp() throws NoSuchMethodException { void scan_componentHasTestListenerMethods() { // when List> channels = - scanner.processChannels(ClassWithTestListenerAnnotation.class).collect(Collectors.toList()); + scanner.process(ClassWithTestListenerAnnotation.class).collect(Collectors.toList()); // then MessagePayload payload = MessagePayload.of(MultiFormatSchema.builder() @@ -118,9 +118,8 @@ private void methodWithoutAnnotation() {} @Test void scan_componentHasMultipleTestListenerMethods() { // when - List> channels = scanner.processChannels( - ClassWithMultipleTestListenerAnnotation.class) - .toList(); + List> channels = + scanner.process(ClassWithMultipleTestListenerAnnotation.class).toList(); // then MessagePayload stringPayload = MessagePayload.of(MultiFormatSchema.builder() diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiIntegrationTest.java index c10541439..c89ade1fb 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiIntegrationTest.java @@ -17,7 +17,7 @@ @SpringBootTest( classes = {SpringwolfAmqpExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ApiIntegrationTest { +class ApiIntegrationTest { @Autowired private TestRestTemplate restTemplate; diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/amqp/SpringwolfAmqpScannerConfiguration.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/amqp/SpringwolfAmqpScannerConfiguration.java index 03b4c27f8..53a189f23 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/amqp/SpringwolfAmqpScannerConfiguration.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/amqp/SpringwolfAmqpScannerConfiguration.java @@ -7,8 +7,11 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.AmqpOperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.ClassLevelAnnotationChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.ClassLevelAnnotationOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.SpringwolfClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersForAmqpBuilder; @@ -76,6 +79,30 @@ public SimpleChannelsScanner simpleRabbitClassLevelListenerAnnotationChannelsSca return new SimpleChannelsScanner(classScanner, strategy); } + @Bean + @ConditionalOnProperty( + name = SPRINGWOLF_SCANNER_RABBIT_LISTENER_ENABLED, + havingValue = "true", + matchIfMissing = true) + @Order(value = ChannelPriority.AUTO_DISCOVERED) + public SimpleOperationsScanner simpleRabbitClassLevelListenerAnnotationOperationsScanner( + SpringwolfClassScanner classScanner, + AmqpBindingFactory amqpBindingBuilder, + AsyncHeadersForAmqpBuilder asyncHeadersForAmqpBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + ClassLevelAnnotationOperationsScanner strategy = + new ClassLevelAnnotationOperationsScanner<>( + RabbitListener.class, + RabbitHandler.class, + amqpBindingBuilder, + asyncHeadersForAmqpBuilder, + payloadClassExtractor, + schemasService); + + return new SimpleOperationsScanner(classScanner, strategy); + } + @Bean @ConditionalOnProperty( name = SPRINGWOLF_SCANNER_RABBIT_LISTENER_ENABLED, @@ -93,6 +120,23 @@ public SimpleChannelsScanner simpleRabbitMethodLevelListenerAnnotationChannelsSc return new SimpleChannelsScanner(classScanner, strategy); } + @Bean + @ConditionalOnProperty( + name = SPRINGWOLF_SCANNER_RABBIT_LISTENER_ENABLED, + havingValue = "true", + matchIfMissing = true) + @Order(value = ChannelPriority.AUTO_DISCOVERED) + public SimpleOperationsScanner simpleRabbitMethodLevelListenerAnnotationOperationsScanner( + SpringwolfClassScanner classScanner, + AmqpBindingFactory amqpBindingBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + MethodLevelAnnotationOperationsScanner strategy = new MethodLevelAnnotationOperationsScanner<>( + RabbitListener.class, amqpBindingBuilder, payloadClassExtractor, schemasService); + + return new SimpleOperationsScanner(classScanner, strategy); + } + @Bean @Order(value = BindingProcessorPriority.PROTOCOL_BINDING) @ConditionalOnMissingBean diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/cloudstream/SpringwolfCloudStreamAutoConfiguration.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/cloudstream/SpringwolfCloudStreamAutoConfiguration.java index 739b38e64..0acd380e1 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/cloudstream/SpringwolfCloudStreamAutoConfiguration.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/cloudstream/SpringwolfCloudStreamAutoConfiguration.java @@ -3,6 +3,7 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.beans.BeanMethodsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream.CloudStreamFunctionChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream.CloudStreamFunctionOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream.FunctionalChannelBeanBuilder; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; @@ -35,6 +36,21 @@ public CloudStreamFunctionChannelsScanner cloudStreamFunctionChannelsScanner( functionalChannelBeanBuilder); } + @Bean + public CloudStreamFunctionOperationsScanner cloudStreamFunctionOperationsScanner( + AsyncApiDocketService asyncApiDocketService, + BeanMethodsScanner beanMethodsScanner, + SchemasService schemasService, + BindingServiceProperties cloudstreamBindingServiceProperties, + FunctionalChannelBeanBuilder functionalChannelBeanBuilder) { + return new CloudStreamFunctionOperationsScanner( + asyncApiDocketService, + beanMethodsScanner, + schemasService, + cloudstreamBindingServiceProperties, + functionalChannelBeanBuilder); + } + @Bean public FunctionalChannelBeanBuilder functionalChannelBeanBuilder(PayloadClassExtractor payloadClassExtractor) { return new FunctionalChannelBeanBuilder(payloadClassExtractor); diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScanner.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScanner.java index f6128228c..896329839 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScanner.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScanner.java @@ -5,20 +5,15 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelMerger; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.bindings.EmptyChannelBinding; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.bindings.EmptyOperationBinding; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.bindings.EmptyMessageBinding; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import io.github.stavshamir.springwolf.asyncapi.v3.bindings.ChannelBinding; import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; -import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelObject; -import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; -import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.MultiFormatSchema; import io.github.stavshamir.springwolf.asyncapi.v3.model.schema.SchemaReference; import io.github.stavshamir.springwolf.asyncapi.v3.model.server.Server; @@ -30,7 +25,6 @@ import org.springframework.cloud.stream.config.BindingServiceProperties; import java.lang.reflect.Method; -import java.util.List; import java.util.Map; import java.util.Set; @@ -45,7 +39,7 @@ public class CloudStreamFunctionChannelsScanner implements ChannelsScanner { private final FunctionalChannelBeanBuilder functionalChannelBeanBuilder; @Override - public Map scanChannels() { + public Map scan() { Set beanMethods = beanMethodsScanner.getBeanMethods(); return ChannelMerger.mergeChannels(beanMethods.stream() .map(functionalChannelBeanBuilder::fromMethodBean) @@ -55,17 +49,6 @@ public Map scanChannels() { .toList()); } - @Override - public Map scanOperations() { - Set beanMethods = beanMethodsScanner.getBeanMethods(); - return ChannelMerger.mergeOperations(beanMethods.stream() - .map(functionalChannelBeanBuilder::fromMethodBean) - .flatMap(Set::stream) - .filter(this::isChannelBean) - .map(this::toOperationEntry) - .toList()); - } - private boolean isChannelBean(FunctionalChannelBeanData beanData) { return cloudStreamBindingsProperties.getBindings().containsKey(beanData.cloudStreamBinding()); } @@ -81,18 +64,6 @@ private Map.Entry toChannelEntry(FunctionalChannelBeanDat return Map.entry(channelName, channelItem); } - private Map.Entry toOperationEntry(FunctionalChannelBeanData beanData) { - String channelName = cloudStreamBindingsProperties - .getBindings() - .get(beanData.cloudStreamBinding()) - .getDestination(); - - String operationId = buildOperationId(beanData, channelName); - Operation operation = buildOperation(beanData, channelName); - - return Map.entry(operationId, operation); - } - private ChannelObject buildChannel(FunctionalChannelBeanData beanData) { Class payloadType = beanData.payloadType(); String modelName = schemasService.registerSchema(payloadType); @@ -118,43 +89,11 @@ private ChannelObject buildChannel(FunctionalChannelBeanData beanData) { .build(); } - private Operation buildOperation(FunctionalChannelBeanData beanData, String channelName) { - Class payloadType = beanData.payloadType(); - String modelName = schemasService.registerSchema(payloadType); - String headerModelName = schemasService.registerSchema(AsyncHeaders.NOT_DOCUMENTED); - - MessageObject message = MessageObject.builder() - .name(payloadType.getName()) - .title(modelName) - .payload(MessagePayload.of(MessageReference.toSchema(modelName))) - .headers(MessageHeaders.of(MessageReference.toSchema(headerModelName))) - .bindings(buildMessageBinding()) - .build(); - - var builder = Operation.builder() - .description("Auto-generated description") - .channel(ChannelReference.fromChannel(channelName)) - .messages(List.of(MessageReference.toChannelMessage(channelName, message))) - .bindings(buildOperationBinding()); - if (beanData.beanType() == FunctionalChannelBeanData.BeanType.CONSUMER) { - builder.action(OperationAction.RECEIVE); - } else { - builder.action(OperationAction.SEND); - } - - return builder.build(); - } - private Map buildMessageBinding() { String protocolName = getProtocolName(); return Map.of(protocolName, new EmptyMessageBinding()); } - private Map buildOperationBinding() { - String protocolName = getProtocolName(); - return Map.of(protocolName, new EmptyOperationBinding()); - } - private Map buildChannelBinding() { String protocolName = getProtocolName(); return Map.of(protocolName, new EmptyChannelBinding()); @@ -174,11 +113,4 @@ private String getProtocolName() { .orElseThrow(() -> new IllegalStateException("There must be at least one server define in the AsyncApiDocker")); } - - private String buildOperationId(FunctionalChannelBeanData beanData, String channelName) { - String operationName = - beanData.beanType() == FunctionalChannelBeanData.BeanType.CONSUMER ? "publish" : "subscribe"; - - return String.format("%s_%s_%s", channelName, operationName, beanData.beanName()); - } } diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionOperationsScanner.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionOperationsScanner.java new file mode 100644 index 000000000..fd7469d01 --- /dev/null +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionOperationsScanner.java @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream; + +import io.github.stavshamir.springwolf.asyncapi.scanners.beans.BeanMethodsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.OperationMerger; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.OperationsScanner; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.bindings.EmptyOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.bindings.EmptyMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.MessageBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.bindings.OperationBinding; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.ChannelReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageHeaders; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageObject; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessagePayload; +import io.github.stavshamir.springwolf.asyncapi.v3.model.channel.message.MessageReference; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.Operation; +import io.github.stavshamir.springwolf.asyncapi.v3.model.operation.OperationAction; +import io.github.stavshamir.springwolf.asyncapi.v3.model.server.Server; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocket; +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.cloud.stream.config.BindingServiceProperties; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Slf4j +@RequiredArgsConstructor +public class CloudStreamFunctionOperationsScanner implements OperationsScanner { + + private final AsyncApiDocketService asyncApiDocketService; + private final BeanMethodsScanner beanMethodsScanner; + private final SchemasService schemasService; + private final BindingServiceProperties cloudStreamBindingsProperties; + private final FunctionalChannelBeanBuilder functionalChannelBeanBuilder; + + @Override + public Map scan() { + Set beanMethods = beanMethodsScanner.getBeanMethods(); + return OperationMerger.mergeOperations(beanMethods.stream() + .map(functionalChannelBeanBuilder::fromMethodBean) + .flatMap(Set::stream) + .filter(this::isChannelBean) + .map(this::toOperationEntry) + .toList()); + } + + private boolean isChannelBean(FunctionalChannelBeanData beanData) { + return cloudStreamBindingsProperties.getBindings().containsKey(beanData.cloudStreamBinding()); + } + + private Map.Entry toOperationEntry(FunctionalChannelBeanData beanData) { + String channelName = cloudStreamBindingsProperties + .getBindings() + .get(beanData.cloudStreamBinding()) + .getDestination(); + + String operationId = buildOperationId(beanData, channelName); + Operation operation = buildOperation(beanData, channelName); + + return Map.entry(operationId, operation); + } + + private Operation buildOperation(FunctionalChannelBeanData beanData, String channelName) { + Class payloadType = beanData.payloadType(); + String modelName = schemasService.registerSchema(payloadType); + String headerModelName = schemasService.registerSchema(AsyncHeaders.NOT_DOCUMENTED); + + MessageObject message = MessageObject.builder() + .name(payloadType.getName()) + .title(modelName) + .payload(MessagePayload.of(MessageReference.toSchema(modelName))) + .headers(MessageHeaders.of(MessageReference.toSchema(headerModelName))) + .bindings(buildMessageBinding()) + .build(); + + var builder = Operation.builder() + .description("Auto-generated description") + .channel(ChannelReference.fromChannel(channelName)) + .messages(List.of(MessageReference.toChannelMessage(channelName, message))) + .bindings(buildOperationBinding()); + if (beanData.beanType() == FunctionalChannelBeanData.BeanType.CONSUMER) { + builder.action(OperationAction.RECEIVE); + } else { + builder.action(OperationAction.SEND); + } + + return builder.build(); + } + + private Map buildMessageBinding() { + String protocolName = getProtocolName(); + return Map.of(protocolName, new EmptyMessageBinding()); + } + + private Map buildOperationBinding() { + String protocolName = getProtocolName(); + return Map.of(protocolName, new EmptyOperationBinding()); + } + + private String getProtocolName() { + AsyncApiDocket docket = asyncApiDocketService.getAsyncApiDocket(); + if (docket.getServers().size() > 1) { + log.warn( + "More than one server has been defined - the channels protocol will be determined by the first one"); + } + + return docket.getServers().entrySet().stream() + .findFirst() + .map(Map.Entry::getValue) + .map(Server::getProtocol) + .orElseThrow(() -> + new IllegalStateException("There must be at least one server define in the AsyncApiDocker")); + } + + private String buildOperationId(FunctionalChannelBeanData beanData, String channelName) { + String operationName = + beanData.beanType() == FunctionalChannelBeanData.BeanType.CONSUMER ? "publish" : "subscribe"; + + return String.format("%s_%s_%s", channelName, operationName, beanData.beanName()); + } +} diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScannerIntegrationTest.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScannerIntegrationTest.java index 34d7d7865..94af68b36 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/CloudStreamFunctionChannelsScannerIntegrationTest.java @@ -62,6 +62,7 @@ ExampleJsonGenerator.class, DefaultAsyncApiDocketService.class, CloudStreamFunctionChannelsScanner.class, + CloudStreamFunctionOperationsScanner.class, FunctionalChannelBeanBuilder.class, SpringwolfConfigProperties.class }) @@ -82,7 +83,10 @@ class CloudStreamFunctionChannelsScannerIntegrationTest { private BindingServiceProperties bindingServiceProperties; @Autowired - private CloudStreamFunctionChannelsScanner scanner; + private CloudStreamFunctionChannelsScanner channelsScanner; + + @Autowired + private CloudStreamFunctionOperationsScanner operationsScanner; @Autowired private SchemasService schemasService; @@ -94,7 +98,7 @@ class CloudStreamFunctionChannelsScannerIntegrationTest { @Test void testNoBindings() { when(bindingServiceProperties.getBindings()).thenReturn(Collections.emptyMap()); - Map channels = scanner.scanChannels(); + Map channels = channelsScanner.scan(); assertThat(channels).isEmpty(); } @@ -107,8 +111,8 @@ void testConsumerBinding() { when(bindingServiceProperties.getBindings()).thenReturn(Map.of("testConsumer-in-0", testConsumerInBinding)); // When scan is called - Map actualChannels = scanner.scanChannels(); - Map actualOperations = scanner.scanOperations(); + Map actualChannels = channelsScanner.scan(); + Map actualOperations = operationsScanner.scan(); // Then the returned channels contain a ChannelItem with the correct data MessageObject message = MessageObject.builder() @@ -149,8 +153,8 @@ void testSupplierBinding() { when(bindingServiceProperties.getBindings()).thenReturn(Map.of("testSupplier-out-0", testSupplierOutBinding)); // When scan is called - Map actualChannels = scanner.scanChannels(); - Map actualOperations = scanner.scanOperations(); + Map actualChannels = channelsScanner.scan(); + Map actualOperations = operationsScanner.scan(); // Then the returned channels contain a ChannelItem with the correct data @@ -201,8 +205,8 @@ void testFunctionBinding() { "testFunction-out-0", testFunctionOutBinding)); // When scan is called - Map actualChannels = scanner.scanChannels(); - Map actualOperations = scanner.scanOperations(); + Map actualChannels = channelsScanner.scan(); + Map actualOperations = operationsScanner.scan(); // Then the returned channels contain a publish ChannelItem and a subscribe ChannelItem MessageObject subscribeMessage = MessageObject.builder() @@ -278,8 +282,8 @@ void testKStreamFunctionBinding() { "kStreamTestFunction-out-0", testFunctionOutBinding)); // When scan is called - Map actualChannels = scanner.scanChannels(); - Map actualOperations = scanner.scanOperations(); + Map actualChannels = channelsScanner.scan(); + Map actualOperations = operationsScanner.scan(); // Then the returned channels contain a publish ChannelItem and a subscribe ChannelItem MessageObject subscribeMessage = MessageObject.builder() @@ -356,8 +360,8 @@ void testFunctionBindingWithSameTopicName() { "testFunction-out-0", testFunctionOutBinding)); // When scan is called - Map actualChannels = scanner.scanChannels(); - Map actualOperations = scanner.scanOperations(); + Map actualChannels = channelsScanner.scan(); + Map actualOperations = operationsScanner.scan(); // Then the returned merged channels contain a publish operation and a subscribe operation MessageObject subscribeMessage = MessageObject.builder() diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java index 27ca0cd55..b752696b3 100644 --- a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java @@ -7,7 +7,9 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.JmsOperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.SpringwolfClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -46,6 +48,20 @@ public SimpleChannelsScanner simpleJmsMethodLevelListenerAnnotationChannelsScann return new SimpleChannelsScanner(classScanner, strategy); } + @Bean + @ConditionalOnProperty(name = SPRINGWOLF_SCANNER_JMS_LISTENER_ENABLED, havingValue = "true", matchIfMissing = true) + @Order(value = ChannelPriority.AUTO_DISCOVERED) + public SimpleOperationsScanner simpleJmsMethodLevelListenerAnnotationOperationsScanner( + SpringwolfClassScanner classScanner, + JmsBindingFactory jmsBindingBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + MethodLevelAnnotationOperationsScanner strategy = new MethodLevelAnnotationOperationsScanner<>( + JmsListener.class, jmsBindingBuilder, payloadClassExtractor, schemasService); + + return new SimpleOperationsScanner(classScanner, strategy); + } + @Bean @Order(value = BindingProcessorPriority.PROTOCOL_BINDING) @ConditionalOnMissingBean diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/kafka/SpringwolfKafkaScannerConfiguration.java b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/kafka/SpringwolfKafkaScannerConfiguration.java index ff605c2b2..d9c671993 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/kafka/SpringwolfKafkaScannerConfiguration.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/kafka/SpringwolfKafkaScannerConfiguration.java @@ -7,8 +7,11 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.KafkaOperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.ClassLevelAnnotationChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.ClassLevelAnnotationOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.SpringwolfClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersForKafkaBuilder; @@ -71,6 +74,30 @@ public SimpleChannelsScanner simpleKafkaClassLevelListenerAnnotationChannelsScan return new SimpleChannelsScanner(classScanner, strategy); } + @Bean + @ConditionalOnProperty( + name = SPRINGWOLF_SCANNER_KAFKA_LISTENER_ENABLED, + havingValue = "true", + matchIfMissing = true) + @Order(value = ChannelPriority.AUTO_DISCOVERED) + public SimpleOperationsScanner simpleKafkaClassLevelListenerAnnotationOperationScanner( + SpringwolfClassScanner classScanner, + KafkaBindingFactory kafkaBindingBuilder, + AsyncHeadersForKafkaBuilder asyncHeadersForKafkaBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + ClassLevelAnnotationOperationsScanner strategy = + new ClassLevelAnnotationOperationsScanner<>( + KafkaListener.class, + KafkaHandler.class, + kafkaBindingBuilder, + asyncHeadersForKafkaBuilder, + payloadClassExtractor, + schemasService); + + return new SimpleOperationsScanner(classScanner, strategy); + } + @Bean @ConditionalOnProperty( name = SPRINGWOLF_SCANNER_KAFKA_LISTENER_ENABLED, @@ -88,6 +115,23 @@ public SimpleChannelsScanner simpleKafkaMethodLevelListenerAnnotationChannelsSca return new SimpleChannelsScanner(classScanner, strategy); } + @Bean + @ConditionalOnProperty( + name = SPRINGWOLF_SCANNER_KAFKA_LISTENER_ENABLED, + havingValue = "true", + matchIfMissing = true) + @Order(value = ChannelPriority.AUTO_DISCOVERED) + public SimpleOperationsScanner simpleKafkaMethodLevelListenerAnnotationOperationsScanner( + SpringwolfClassScanner classScanner, + KafkaBindingFactory kafkaBindingBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + MethodLevelAnnotationOperationsScanner strategy = new MethodLevelAnnotationOperationsScanner<>( + KafkaListener.class, kafkaBindingBuilder, payloadClassExtractor, schemasService); + + return new SimpleOperationsScanner(classScanner, strategy); + } + @Bean @Order(value = BindingProcessorPriority.PROTOCOL_BINDING) @ConditionalOnMissingBean diff --git a/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/sqs/SpringwolfSqsScannerConfiguration.java b/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/sqs/SpringwolfSqsScannerConfiguration.java index 821df83a4..501fedf85 100644 --- a/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/sqs/SpringwolfSqsScannerConfiguration.java +++ b/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/sqs/SpringwolfSqsScannerConfiguration.java @@ -8,7 +8,9 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.SqsOperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.SimpleOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelAnnotationOperationsScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.SpringwolfClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -46,6 +48,20 @@ public SimpleChannelsScanner simpleSqsMethodLevelListenerAnnotationChannelsScann return new SimpleChannelsScanner(classScanner, strategy); } + @Bean + @ConditionalOnProperty(name = SPRINGWOLF_SCANNER_SQS_LISTENER_ENABLED, havingValue = "true", matchIfMissing = true) + @Order(value = ChannelPriority.AUTO_DISCOVERED) + public SimpleOperationsScanner simpleSqsMethodLevelListenerAnnotationOperationsScanner( + SpringwolfClassScanner classScanner, + SqsBindingFactory sqsBindingBuilder, + PayloadClassExtractor payloadClassExtractor, + SchemasService schemasService) { + MethodLevelAnnotationOperationsScanner strategy = new MethodLevelAnnotationOperationsScanner<>( + SqsListener.class, sqsBindingBuilder, payloadClassExtractor, schemasService); + + return new SimpleOperationsScanner(classScanner, strategy); + } + @Bean @Order(value = BindingProcessorPriority.PROTOCOL_BINDING) @ConditionalOnMissingBean