From 754474fa27bdd268a9d0e118515e6c18a5dedfc5 Mon Sep 17 00:00:00 2001 From: Thomas Vahrst Date: Fri, 10 Nov 2023 13:53:51 +0100 Subject: [PATCH] support channel server assignments (#264) * Allow server assignments to channels in @AsyncOperation * added comment. * fix import * fix broken javadoc in AsyncOperation * merge master changes * validate server refs in channels * validate server refs in channels * make servers property in OperationData Nullable --- .../SpringwolfScannerConfiguration.java | 14 ++++- .../AbstractOperationDataScanner.java | 55 +++++++++++++++++ .../ConsumerOperationDataScanner.java | 5 ++ .../ProducerOperationDataScanner.java | 5 ++ .../AsyncAnnotationScannerUtil.java | 14 +++++ .../AsyncListenerAnnotationScanner.java | 9 +++ .../annotation/AsyncOperation.java | 8 +++ .../AsyncPublisherAnnotationScanner.java | 8 +++ .../asyncapi/types/ConsumerData.java | 11 ++++ .../asyncapi/types/OperationData.java | 9 +++ .../asyncapi/types/ProducerData.java | 11 ++++ ...ncApiSerializerServiceIntegrationTest.java | 1 + ...erOperationDataScannerIntegrationTest.java | 24 ++++++++ ...erOperationDataScannerIntegrationTest.java | 23 +++++++ .../AsyncAnnotationScannerUtilTest.java | 15 +++++ ...tenerAnnotationScannerIntegrationTest.java | 60 +++++++++++++++++-- ...isherAnnotationScannerIntegrationTest.java | 4 ++ .../src/test/resources/asyncapi/asyncapi.json | 3 + .../src/test/resources/asyncapi/asyncapi.yaml | 2 + 19 files changed, 275 insertions(+), 6 deletions(-) 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 0f684a8e5..002e1cf9c 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 @@ -79,10 +79,15 @@ public ProducerOperationDataScanner producerOperationDataScanner( public AsyncListenerAnnotationScanner asyncListenerAnnotationScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, + AsyncApiDocketService asyncApiDocketService, List operationBindingProcessors, List messageBindingProcessors) { return new AsyncListenerAnnotationScanner( - componentClassScanner, schemasService, operationBindingProcessors, messageBindingProcessors); + componentClassScanner, + schemasService, + asyncApiDocketService, + operationBindingProcessors, + messageBindingProcessors); } @Bean @@ -94,9 +99,14 @@ public AsyncListenerAnnotationScanner asyncListenerAnnotationScanner( public AsyncPublisherAnnotationScanner asyncPublisherAnnotationScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, + AsyncApiDocketService asyncApiDocketService, List operationBindingProcessors, List messageBindingProcessors) { return new AsyncPublisherAnnotationScanner( - componentClassScanner, schemasService, operationBindingProcessors, messageBindingProcessors); + componentClassScanner, + schemasService, + asyncApiDocketService, + operationBindingProcessors, + messageBindingProcessors); } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/AbstractOperationDataScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/AbstractOperationDataScanner.java index f911998ea..e7f7f580d 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/AbstractOperationDataScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/AbstractOperationDataScanner.java @@ -3,6 +3,7 @@ import com.asyncapi.v2._6_0.model.channel.ChannelItem; import com.asyncapi.v2._6_0.model.channel.operation.Operation; +import com.asyncapi.v2._6_0.model.server.Server; import com.asyncapi.v2.binding.channel.ChannelBinding; import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; @@ -10,6 +11,7 @@ import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.schemas.SchemasService; import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; @@ -31,6 +33,14 @@ public abstract class AbstractOperationDataScanner implements ChannelsScanner { protected abstract SchemasService getSchemaService(); + protected abstract AsyncApiDocketService getAsyncApiDocketService(); + + /** + * provides a list of all {@link OperationData} items detected by the concrete + * subclass. + * + * @return + */ protected abstract List getOperationData(); protected abstract OperationData.OperationType getOperationType(); @@ -57,6 +67,15 @@ private boolean allFieldsAreNonNull(OperationData operationData) { return allNonNull; } + /** + * Creates an asyncapi {@link ChannelItem} using the given list of {@link OperationData}. Expects, that all {@link OperationData} + * items belong to the same channel. Most properties of the resulting {@link ChannelItem} are extracted from the + * first {@link OperationData} item in the list, assuming that all {@link OperationData} contains the same channel + * informations. + * + * @param operationDataList List of all {@link OperationData} items for a single channel. + * @return the resulting {@link ChannelItem} + */ private ChannelItem buildChannel(List operationDataList) { // All bindings in the group are assumed to be the same // AsyncApi does not support multiple bindings on a single channel @@ -68,6 +87,7 @@ private ChannelItem buildChannel(List operationDataList) { Map chBinding = channelBinding != null ? new HashMap<>(channelBinding) : null; String operationId = operationDataList.get(0).getChannelName() + "_" + this.getOperationType().operationName; String description = operationDataList.get(0).getDescription(); + List servers = operationDataList.get(0).getServers(); if (description.isEmpty()) { description = "Auto-generated description"; @@ -84,6 +104,14 @@ private ChannelItem buildChannel(List operationDataList) { channelBuilder = switch (getOperationType()) { case PUBLISH -> channelBuilder.publish(operation); case SUBSCRIBE -> channelBuilder.subscribe(operation);}; + + // Only set servers if servers are defined. Avoid setting an emtpy list + // because this would generate empty server entries for each channel in the resulting + // async api. + if (servers != null && !servers.isEmpty()) { + validateServers(servers, operationId); + channelBuilder.servers(servers); + } return channelBuilder.build(); } @@ -148,4 +176,31 @@ private void processAsyncMessageAnnotation(Message annotationMessage, Message.Me } } } + + /** + * validates the given list of server names (for a specific operation) with the servers defined in the 'servers' part of + * the current AsyncApi. + * + * @param serversFromOperation the server names defined for the current operation + * @param operationId operationId of the current operation - used for exception messages + * @throws IllegalArgumentException if server from operation is not present in AsyncApi's servers definition. + */ + void validateServers(List serversFromOperation, String operationId) { + if (!serversFromOperation.isEmpty()) { + Map asyncApiServers = + getAsyncApiDocketService().getAsyncApiDocket().getServers(); + if (asyncApiServers == null || asyncApiServers.isEmpty()) { + throw new IllegalArgumentException(String.format( + "Operation '%s' defines server refs (%s) but there are no servers defined in this AsyncAPI.", + operationId, serversFromOperation)); + } + for (String server : serversFromOperation) { + if (!asyncApiServers.containsKey(server)) { + throw new IllegalArgumentException(String.format( + "Operation '%s' defines unknown server ref '%s'. This AsyncApi defines these server(s): %s", + operationId, server, asyncApiServers.keySet())); + } + } + } + } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScanner.java index 269d1ad5f..95d9999ea 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScanner.java @@ -28,6 +28,11 @@ protected List getOperationData() { return new ArrayList<>(asyncApiDocketService.getAsyncApiDocket().getConsumers()); } + @Override + protected AsyncApiDocketService getAsyncApiDocketService() { + return this.asyncApiDocketService; + } + @Override protected OperationData.OperationType getOperationType() { return OperationData.OperationType.PUBLISH; diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScanner.java index c66e02735..cbd13ec16 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScanner.java @@ -23,6 +23,11 @@ protected SchemasService getSchemaService() { return this.schemasService; } + @Override + protected AsyncApiDocketService getAsyncApiDocketService() { + return this.asyncApiDocketService; + } + @Override protected List getOperationData() { return new ArrayList<>(asyncApiDocketService.getAsyncApiDocket().getProducers()); diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtil.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtil.java index 86ecae58a..82a4da2b9 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtil.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtil.java @@ -10,6 +10,7 @@ import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaderSchema; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; +import org.springframework.lang.NonNull; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -122,4 +123,17 @@ private static Message parseMessage(AsyncOperation asyncOperation) { return messageBuilder.build(); } + + /** + * extracts servers array from the given AsyncOperation, resolves placeholdes with spring variables and + * return a List of server names. + * + * @param op the given AsyncOperation + * @param resolver the StringValueResolver to resolve placeholders + * @return List of server names + */ + @NonNull + public static List getServers(AsyncOperation op, StringValueResolver resolver) { + return Arrays.stream(op.servers()).map(resolver::resolveStringValue).collect(Collectors.toList()); + } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java index 065730ea6..d6821f053 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScanner.java @@ -11,6 +11,7 @@ import io.github.stavshamir.springwolf.asyncapi.types.ConsumerData; import io.github.stavshamir.springwolf.asyncapi.types.OperationData; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.schemas.SchemasService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -30,6 +31,7 @@ public class AsyncListenerAnnotationScanner extends AbstractOperationDataScanner private StringValueResolver resolver; private final ComponentClassScanner componentClassScanner; private final SchemasService schemasService; + private final AsyncApiDocketService asyncApiDocketService; private final List operationBindingProcessors; @@ -45,6 +47,11 @@ protected SchemasService getSchemaService() { return this.schemasService; } + @Override + protected AsyncApiDocketService getAsyncApiDocketService() { + return asyncApiDocketService; + } + @Override protected List getOperationData() { return componentClassScanner.scan().stream() @@ -87,9 +94,11 @@ private ConsumerData toConsumerData( Class payloadType = op.payloadType() != Object.class ? op.payloadType() : SpringPayloadAnnotationTypeExtractor.getPayloadType(method); + return ConsumerData.builder() .channelName(resolver.resolveStringValue(op.channelName())) .description(resolver.resolveStringValue(op.description())) + .servers(AsyncAnnotationScannerUtil.getServers(op, resolver)) .headers(AsyncAnnotationScannerUtil.getAsyncHeaders(op, resolver)) .payloadType(payloadType) .operationBinding(operationBindings) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncOperation.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncOperation.java index 8b8c52662..c88634535 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncOperation.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncOperation.java @@ -30,6 +30,14 @@ */ AsyncMessage message() default @AsyncMessage(); + /** + * The servers on which this channel is available, list of names of Server Objects. + * If servers is empty then this channel is available on all servers defined in the Async API. + * Mapped to ({@link OperationData#getServers()} + * @return + */ + String[] servers() default {}; + /** * Mapped to {@link OperationData#getPayloadType()} */ diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java index 142e3119d..596547404 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScanner.java @@ -11,6 +11,7 @@ import io.github.stavshamir.springwolf.asyncapi.types.OperationData; import io.github.stavshamir.springwolf.asyncapi.types.ProducerData; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.schemas.SchemasService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -30,6 +31,7 @@ public class AsyncPublisherAnnotationScanner extends AbstractOperationDataScanne private StringValueResolver resolver; private final ComponentClassScanner componentClassScanner; private final SchemasService schemasService; + private final AsyncApiDocketService asyncApiDocketService; private final List operationBindingProcessors; @@ -45,6 +47,11 @@ protected SchemasService getSchemaService() { return this.schemasService; } + @Override + protected AsyncApiDocketService getAsyncApiDocketService() { + return asyncApiDocketService; + } + @Override protected List getOperationData() { return componentClassScanner.scan().stream() @@ -90,6 +97,7 @@ private ProducerData toConsumerData( return ProducerData.builder() .channelName(resolver.resolveStringValue(op.channelName())) .description(resolver.resolveStringValue(op.description())) + .servers(AsyncAnnotationScannerUtil.getServers(op, resolver)) .headers(AsyncAnnotationScannerUtil.getAsyncHeaders(op, resolver)) .payloadType(payloadType) .operationBinding(operationBindings) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ConsumerData.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ConsumerData.java index 123c01124..2261fe98d 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ConsumerData.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ConsumerData.java @@ -10,7 +10,10 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.Singular; +import org.springframework.lang.Nullable; +import java.util.List; import java.util.Map; /** @@ -33,6 +36,14 @@ public class ConsumerData implements OperationData { */ protected String description; + /** + * Optional, List of server names the channel is assigned to. If empty, the + * channel is available on all defined servers. + */ + @Nullable + @Singular("server") + protected List servers; + /** * The channel binding of the producer. *
diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/OperationData.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/OperationData.java index fc2946c01..44ecd56b9 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/OperationData.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/OperationData.java @@ -6,7 +6,9 @@ import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; +import org.springframework.lang.Nullable; +import java.util.List; import java.util.Map; /** @@ -23,6 +25,13 @@ public interface OperationData { */ String getDescription(); + /** + * Optional, List of server names the channel is assigned to. If empty, the + * channel is available on all defined servers. May be null. + */ + @Nullable + List getServers(); + /** * The channel binding. */ diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ProducerData.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ProducerData.java index 46279568f..bd355e0aa 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ProducerData.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/types/ProducerData.java @@ -10,7 +10,10 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.Singular; +import org.springframework.lang.Nullable; +import java.util.List; import java.util.Map; /** @@ -33,6 +36,14 @@ public class ProducerData implements OperationData { */ protected String description; + /** + * Optional, List of server names the channel is assigned to. If empty, the + * channel is available on all defined servers. + */ + @Nullable + @Singular("server") + protected List servers; + /** * The channel binding of the producer. *
diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiSerializerServiceIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiSerializerServiceIntegrationTest.java index 21146c826..891748eae 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiSerializerServiceIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiSerializerServiceIntegrationTest.java @@ -87,6 +87,7 @@ private AsyncAPI getAsyncAPITestObject() { ChannelItem newUserChannel = ChannelItem.builder() .description("This channel is used to exchange messages about users signing up") + .servers(List.of("production")) .subscribe(newUserOperation) .build(); diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScannerIntegrationTest.java index 07b46bf47..b6eff4e33 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ConsumerOperationDataScannerIntegrationTest.java @@ -4,6 +4,7 @@ import com.asyncapi.v2._6_0.model.channel.ChannelItem; import com.asyncapi.v2._6_0.model.channel.operation.Operation; import com.asyncapi.v2._6_0.model.info.Info; +import com.asyncapi.v2._6_0.model.server.Server; import com.asyncapi.v2.binding.channel.kafka.KafkaChannelBinding; import com.asyncapi.v2.binding.message.kafka.KafkaMessageBinding; import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; @@ -18,6 +19,7 @@ import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator; import io.swagger.v3.oas.annotations.media.Schema; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -32,6 +34,7 @@ import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessageObjectOrComposition; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @@ -50,6 +53,20 @@ class ConsumerOperationDataScannerIntegrationTest { @MockBean private AsyncApiDocketService asyncApiDocketService; + @BeforeEach + public void defaultDocketSetup() { + AsyncApiDocket docket = AsyncApiDocket.builder() + .info(Info.builder() + .title("Default Asyncapi Title") + .version("1.0.0") + .build()) + .server("kafka1", new Server()) + .server("kafka2", new Server()) + .build(); + + when(asyncApiDocketService.getAsyncApiDocket()).thenReturn(docket); + } + @Test void allFieldsConsumerData() { // Given a consumer data with all fields set @@ -121,6 +138,7 @@ void multipleConsumersForSameTopic() { ConsumerData consumerData1 = ConsumerData.builder() .channelName(channelName) .description(description1) + .server("kafka1") .channelBinding(Map.of("kafka", new KafkaChannelBinding())) .operationBinding(Map.of("kafka", new KafkaOperationBinding())) .messageBinding(Map.of("kafka", new KafkaMessageBinding())) @@ -130,6 +148,7 @@ void multipleConsumersForSameTopic() { ConsumerData consumerData2 = ConsumerData.builder() .channelName(channelName) .description(description2) + .server("kafka2") .channelBinding(Map.of("kafka", new KafkaChannelBinding())) .operationBinding(Map.of("kafka", new KafkaOperationBinding())) .messageBinding(Map.of("kafka", new KafkaMessageBinding())) @@ -173,6 +192,7 @@ void multipleConsumersForSameTopic() { .build(); ChannelItem expectedChannel = ChannelItem.builder() + .servers(List.of("kafka1")) // First Consumerdata Server Entry .bindings(Map.of("kafka", new KafkaChannelBinding())) .publish(operation) .build(); @@ -186,8 +206,12 @@ private void mockConsumers(Collection consumers) { .title("ConsumerOperationDataScannerTest-title") .version("ConsumerOperationDataScannerTest-version") .build()) + .server("kafka1", new Server()) + .server("kafka2", new Server()) .consumers(consumers) .build(); + + reset(asyncApiDocketService); when(asyncApiDocketService.getAsyncApiDocket()).thenReturn(asyncApiDocket); } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScannerIntegrationTest.java index 4fff0c5dd..237220b1d 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/ProducerOperationDataScannerIntegrationTest.java @@ -4,6 +4,7 @@ import com.asyncapi.v2._6_0.model.channel.ChannelItem; import com.asyncapi.v2._6_0.model.channel.operation.Operation; import com.asyncapi.v2._6_0.model.info.Info; +import com.asyncapi.v2._6_0.model.server.Server; import com.asyncapi.v2.binding.channel.kafka.KafkaChannelBinding; import com.asyncapi.v2.binding.message.kafka.KafkaMessageBinding; import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; @@ -18,6 +19,7 @@ import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator; import io.swagger.v3.oas.annotations.media.Schema; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -32,6 +34,7 @@ import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessageObjectOrComposition; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @@ -50,6 +53,20 @@ class ProducerOperationDataScannerIntegrationTest { @MockBean private AsyncApiDocketService asyncApiDocketService; + @BeforeEach + public void defaultDocketSetup() { + AsyncApiDocket docket = AsyncApiDocket.builder() + .info(Info.builder() + .title("Default Asyncapi Title") + .version("1.0.0") + .build()) + .server("kafka1", new Server()) + .server("kafka2", new Server()) + .build(); + + when(asyncApiDocketService.getAsyncApiDocket()).thenReturn(docket); + } + @Test void allFieldsProducerData() { // Given a producer data with all fields set @@ -121,6 +138,7 @@ void multipleProducersForSameTopic() { ProducerData producerData1 = ProducerData.builder() .channelName(channelName) .description(description1) + .server("kafka1") .channelBinding(Map.of("kafka", new KafkaChannelBinding())) .operationBinding(Map.of("kafka", new KafkaOperationBinding())) .messageBinding(Map.of("kafka", new KafkaMessageBinding())) @@ -130,6 +148,7 @@ void multipleProducersForSameTopic() { ProducerData producerData2 = ProducerData.builder() .channelName(channelName) .description(description2) + .server("kafka2") .channelBinding(Map.of("kafka", new KafkaChannelBinding())) .operationBinding(Map.of("kafka", new KafkaOperationBinding())) .messageBinding(Map.of("kafka", new KafkaMessageBinding())) @@ -173,6 +192,7 @@ void multipleProducersForSameTopic() { .build(); ChannelItem expectedChannel = ChannelItem.builder() + .servers(List.of("kafka1")) // First Consumerdata Server Entry .bindings(Map.of("kafka", new KafkaChannelBinding())) .subscribe(operation) .build(); @@ -186,8 +206,11 @@ private void mockProducers(Collection producers) { .title("ProducerOperationDataScannerTest-title") .version("ProducerOperationDataScannerTest-version") .build()) + .server("kafka1", new Server()) + .server("kafka2", new Server()) .producers(producers) .build(); + reset(asyncApiDocketService); when(asyncApiDocketService.getAsyncApiDocket()).thenReturn(asyncApiDocket); } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtilTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtilTest.java index 1b56e92e3..539c264a7 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtilTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncAnnotationScannerUtilTest.java @@ -147,12 +147,27 @@ void processMessageFromAnnotationWithAsyncMessage(Class classWithOperationBin assertEquals(expectedMessage, message); } + @Test + void getServers() throws NoSuchMethodException { + Method m = ClassWithOperationBindingProcessor.class.getDeclaredMethod("methodWithAnnotation", String.class); + AsyncOperation operation = m.getAnnotation(AsyncListener.class).operation(); + + StringValueResolver resolver = mock(StringValueResolver.class); + + // when + when(resolver.resolveStringValue("${test.property.server1}")).thenReturn("server1"); + + List servers = AsyncAnnotationScannerUtil.getServers(operation, resolver); + assertEquals(List.of("server1"), servers); + } + private static class ClassWithOperationBindingProcessor { @AsyncListener( operation = @AsyncOperation( channelName = "${test.property.test-channel}", description = "${test.property.description}", + servers = {"${test.property.server1}"}, headers = @AsyncOperation.Headers( schemaName = "TestSchema", diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java index 6bd75763d..17f03974e 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncListenerAnnotationScannerIntegrationTest.java @@ -3,18 +3,23 @@ import com.asyncapi.v2._6_0.model.channel.ChannelItem; import com.asyncapi.v2._6_0.model.channel.operation.Operation; +import com.asyncapi.v2._6_0.model.info.Info; +import com.asyncapi.v2._6_0.model.server.Server; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.TestOperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference; +import io.github.stavshamir.springwolf.configuration.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.example.ExampleJsonGenerator; 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.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -23,12 +28,14 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.List; import java.util.Map; import java.util.Set; import static java.util.Collections.EMPTY_MAP; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @@ -40,8 +47,14 @@ SpringwolfConfigProperties.class, TestOperationBindingProcessor.class }) -@TestPropertySource(properties = {"test.property.test-channel=test-channel", "test.property.description=description"}) -class AsyncListenerAnnotationScannerIntegrationTest { +@TestPropertySource( + properties = { + "test.property.test-channel=test-channel", + "test.property.description=description", + "test.property.server1=server1", + "test.property.server2=server2" + }) +class AsyncListenerAnnotationScannerTest { @Autowired private AsyncListenerAnnotationScanner channelScanner; @@ -49,11 +62,24 @@ class AsyncListenerAnnotationScannerIntegrationTest { @MockBean private ComponentClassScanner componentClassScanner; + @MockBean + private AsyncApiDocketService asyncApiDocketService; + private void setClassToScan(Class classToScan) { Set> classesToScan = singleton(classToScan); when(componentClassScanner.scan()).thenReturn(classesToScan); } + @BeforeEach + public void setup() { + when(asyncApiDocketService.getAsyncApiDocket()) + .thenReturn(AsyncApiDocket.builder() + .info(new Info()) + .server("server1", new Server()) + .server("server2", new Server()) + .build()); + } + @Test void scan_componentHasNoListenerMethods() { setClassToScan(ClassWithoutListenerAnnotation.class); @@ -95,6 +121,17 @@ void scan_componentHasListenerMethod() { assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); } + @Test + void scan_componentHasListenerMethodWithUnknownServer() { + // Given a class with method annotated with AsyncListener, with an unknown servername + setClassToScan(ClassWithListenerAnnotationWithInvalidServer.class); + + assertThatThrownBy(() -> channelScanner.scan()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Operation 'test-channel_publish' defines unknown server ref 'server3'. This AsyncApi defines these server(s): [server1, server2]"); + } + @Test void scan_componentHasListenerMethodWithAllAttributes() { // Given a class with method annotated with AsyncListener, where all attributes are set @@ -121,8 +158,11 @@ void scan_componentHasListenerMethodWithAllAttributes() { .message(message) .build(); - ChannelItem expectedChannel = - ChannelItem.builder().bindings(null).publish(operation).build(); + ChannelItem expectedChannel = ChannelItem.builder() + .bindings(null) + .servers(List.of("server1", "server2")) + .publish(operation) + .build(); assertThat(actualChannels).containsExactly(Map.entry("test-channel", expectedChannel)); } @@ -220,6 +260,17 @@ 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( @@ -227,6 +278,7 @@ private static class ClassWithListenerAnnotationWithAllAttributes { @AsyncOperation( channelName = "${test.property.test-channel}", description = "${test.property.description}", + servers = {"${test.property.server1}", "${test.property.server2}"}, headers = @AsyncOperation.Headers( schemaName = "TestSchema", diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java index 53a995f60..1c16ec570 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/AsyncPublisherAnnotationScannerIntegrationTest.java @@ -9,6 +9,7 @@ import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator; @@ -48,6 +49,9 @@ class AsyncPublisherAnnotationScannerIntegrationTest { @MockBean private ComponentClassScanner componentClassScanner; + @MockBean + private AsyncApiDocketService asyncApiDocketService; + private void setClassToScan(Class classToScan) { Set> classesToScan = singleton(classToScan); when(componentClassScanner.scan()).thenReturn(classesToScan); diff --git a/springwolf-core/src/test/resources/asyncapi/asyncapi.json b/springwolf-core/src/test/resources/asyncapi/asyncapi.json index 4487cee67..598ea8e19 100644 --- a/springwolf-core/src/test/resources/asyncapi/asyncapi.json +++ b/springwolf-core/src/test/resources/asyncapi/asyncapi.json @@ -27,6 +27,9 @@ "channels": { "new-user": { "description": "This channel is used to exchange messages about users signing up", + "servers": [ + "production" + ], "subscribe": { "operationId": "new-user_listenerMethod_subscribe", "bindings": { diff --git a/springwolf-core/src/test/resources/asyncapi/asyncapi.yaml b/springwolf-core/src/test/resources/asyncapi/asyncapi.yaml index 422d9ffd8..abf1d3f90 100644 --- a/springwolf-core/src/test/resources/asyncapi/asyncapi.yaml +++ b/springwolf-core/src/test/resources/asyncapi/asyncapi.yaml @@ -21,6 +21,8 @@ servers: channels: new-user: description: This channel is used to exchange messages about users signing up + servers: + - production subscribe: operationId: new-user_listenerMethod_subscribe bindings: