diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b805c3e0..4ce812e3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,10 +34,11 @@ Contributing follows mostly the following steps: 1. For bigger changes, open an [issue](https://github.com/springwolf/springwolf-core/issues) and/or drop by on our discord so that we can coordinate. 2. Fork the repository + - Create a new branch 3. Apply your changes - Add/Adapt tests as necessary 4. Run the tests - - Run tests locally via `./gradlew test` + - Run tests locally via `./gradlew test` (includes `unitTest` and `integrationTest` targets, which executes faster) 5. Run the code formatter - We use the palantir code style and disallow wildcard imports - Run `./gradlew spotlessApply` to fix most things automatically @@ -45,4 +46,5 @@ Contributing follows mostly the following steps: 7. Open a Pull-Request against the springwolf repository - Use the provided template to briefly describe why the changes should be included 8. Update the [documentation](https://github.com/springwolf/springwolf.github.io) as necessary +9. Add yourself to the contributors and your company to the users in [README.md](README.md) diff --git a/README.md b/README.md index f114bed47..cd9297875 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ **Automated documentation for event-driven applications built with Spring Boot** +![Last Version](https://img.shields.io/github/tag-pre/springwolf/springwolf-core.svg) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![springwolf-core](https://github.com/springwolf/springwolf-core/actions/workflows/springwolf-core.yml/badge.svg)](https://github.com/springwolf/springwolf-core/actions/workflows/springwolf-core.yml) [![springwolf-ui](https://github.com/springwolf/springwolf-core/actions/workflows/springwolf-ui.yml/badge.svg)](https://github.com/springwolf/springwolf-core/actions/workflows/springwolf-ui.yml) @@ -16,6 +17,7 @@ - [Demo & Documentation](#demo--documentation) - [Why You Should Use It](#why-you-should-use-it) - [Usage & Example](#usage--example) +- [Who's Using Springwolf](#whos-using-springwolf) - [Development](#development) - [Contributors](#contributors) @@ -37,12 +39,12 @@ The documentation and quickstart is available on [springwolf.dev](https://www.sp Springwolf exploits the fact that you already fully described your consumer endpoint (with listener annotations, such as `@KafkaListener`, `@RabbitListener`, `@SqsListener`, etc.) and generates the documentation based on this information. -#### Share API Schema Definition +#### 1. Share API Schema Definition The AsyncAPI conform documentation can be integrated into API hubs (like [backstage](https://backstage.io/docs/features/software-catalog/descriptor-format/)) or be shared as a `json`/`yaml` file with others. -#### UI Based API Testing +#### 2. UI Based API Testing In projects using asynchronous APIs, you may often find yourself needing to manually send a message to some topic, whether you are manually testing a new feature, debugging or trying to understand some flow. @@ -66,8 +68,22 @@ More details in the documentation. | [Generic Binding](https://github.com/springwolf/springwolf-core/tree/master/springwolf-add-ons/springwolf-generic-binding) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-generic-binding?color=green&label=springwolf-generic-binding&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-generic-binding?label=springwolf-generic-binding&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Json Schema](https://github.com/springwolf/springwolf-core/tree/master/springwolf-add-ons/springwolf-json-schema) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-json-schema?color=green&label=springwolf-json-schema&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-json-schema?label=springwolf-json-schema&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | +### Who's Using Springwolf + +- [b.well Connected Health](https://www.icanbwell.com/) +- [LVM Versicherung](https://www.lvm.de/privatkunden/) +- [OTTO](https://www.otto.de) +- [Teambank](https://www.teambank.de) + +Comment in [this PR](https://github.com/springwolf/springwolf-core/issues/342) to add your company and spread the word + ### Development +Check out our [CONTRIBUTING.md](CONTRIBUTING.md) guide. + +
+Using SNAPSHOT version + #### Sonatype snapshots Add the following to the `repositories` closure in `build.gradle`: @@ -100,6 +116,7 @@ Or add the `repository` to your `pom.xml` if you are using maven: #### Local Snapshot Build To work with local builds, run the `publishToMavenLocal` task. The current version number is set in [`.env`](.env) file. +
### Contributors diff --git a/build.gradle b/build.gradle index 96832fce3..5726e7980 100644 --- a/build.gradle +++ b/build.gradle @@ -8,8 +8,8 @@ buildscript { plugins { id 'ca.cutterslade.analyze' version '1.9.1' id 'io.spring.dependency-management' version '1.1.4' apply false - id 'org.springframework.boot' version '3.1.5' apply false - id 'org.owasp.dependencycheck' version '8.4.2' + id 'org.springframework.boot' version '3.2.0' apply false + id 'org.owasp.dependencycheck' version '8.4.3' id 'com.diffplug.spotless' version '6.22.0' id 'com.bmuschko.docker-spring-boot-application' version '9.3.7' apply false } @@ -66,6 +66,9 @@ allprojects { test { dependsOn spotlessApply // Automatically fix code formatting if possible + minHeapSize = "128m" // initial heap size + maxHeapSize = "1024m" // maximum heap size + useJUnitPlatform() testLogging { // showStandardStreams = true diff --git a/dependencies.gradle b/dependencies.gradle index f291dc966..01ead48af 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -25,22 +25,20 @@ * The dependencies are sorted alphabetically. */ ext { - androidJsonVersion = '0.0.20131108.vaadin1' - assertjCoreVersion = '3.24.2' asyncapiCoreVersion = '1.0.0-EAP-2' awaitilityVersion = '4.2.0' - commonsLang3Version = '3.13.0' + commonsLang3Version = '3.14.0' jsr305Version = '3.0.2' kafkaClientsVersion = '3.6.0' kafkaStreamsVersion = '3.6.0' - jacksonVersion = '2.15.3' + jacksonVersion = '2.16.0' jakartaAnnotationApiVersion = '2.1.1' jsonSchemaValidator = '1.0.87' @@ -59,5 +57,5 @@ ext { swaggerVersion = '2.2.19' - testcontainersVersion = '1.19.1' + testcontainersVersion = '1.19.3' } diff --git a/springwolf-core/build.gradle b/springwolf-core/build.gradle index 686276aba..78a6b6a98 100644 --- a/springwolf-core/build.gradle +++ b/springwolf-core/build.gradle @@ -47,7 +47,6 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-params:${junitJupiterVersion}") testImplementation "org.assertj:assertj-core:${assertjCoreVersion}" testImplementation "org.awaitility:awaitility:${awaitilityVersion}" - testImplementation "com.vaadin.external.google:android-json:${androidJsonVersion}" testImplementation "org.mockito:mockito-core:${mockitoCoreVersion}" testImplementation "org.springframework.boot:spring-boot-test" testImplementation "org.springframework:spring-test" 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 ab9d3ca81..3ce88cfca 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 @@ -8,6 +8,7 @@ import io.github.stavshamir.springwolf.asyncapi.DefaultChannelsService; import io.github.stavshamir.springwolf.asyncapi.SpringwolfInitApplicationListener; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.configuration.AsyncApiDocket; import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.configuration.DefaultAsyncApiDocketService; @@ -86,4 +87,10 @@ public AsyncApiDocketService asyncApiDocketService( public ExampleGenerator exampleGenerator() { return new ExampleJsonGenerator(); } + + @Bean + @ConditionalOnMissingBean + public PayloadClassExtractor payloadClassExtractor(SpringwolfConfigProperties springwolfConfigProperties) { + return new PayloadClassExtractor(springwolfConfigProperties); + } } 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 002e1cf9c..872cb5d0b 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 @@ -10,6 +10,7 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProducerOperationDataScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListenerAnnotationScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisherAnnotationScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ConfigurationClassScanner; import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; @@ -80,12 +81,14 @@ public AsyncListenerAnnotationScanner asyncListenerAnnotationScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, AsyncApiDocketService asyncApiDocketService, + PayloadClassExtractor payloadClassExtractor, List operationBindingProcessors, List messageBindingProcessors) { return new AsyncListenerAnnotationScanner( componentClassScanner, schemasService, asyncApiDocketService, + payloadClassExtractor, operationBindingProcessors, messageBindingProcessors); } @@ -100,12 +103,14 @@ public AsyncPublisherAnnotationScanner asyncPublisherAnnotationScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, AsyncApiDocketService asyncApiDocketService, + PayloadClassExtractor payloadClassExtractor, List operationBindingProcessors, List messageBindingProcessors) { return new AsyncPublisherAnnotationScanner( componentClassScanner, schemasService, asyncApiDocketService, + payloadClassExtractor, operationBindingProcessors, messageBindingProcessors); } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractClassLevelListenerScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractClassLevelListenerScanner.java index f7ae77fb4..3f6a818d0 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractClassLevelListenerScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractClassLevelListenerScanner.java @@ -8,6 +8,7 @@ import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelMerger; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; 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; @@ -35,7 +36,6 @@ import java.util.stream.Collectors; import static io.github.stavshamir.springwolf.asyncapi.MessageHelper.toMessageObjectOrComposition; -import static io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.SpringPayloadAnnotationTypeExtractor.getPayloadType; import static java.util.stream.Collectors.toSet; @Slf4j @@ -48,6 +48,8 @@ public abstract class AbstractClassLevelListenerScanner< private final SchemasService schemasService; + protected final PayloadClassExtractor payloadClassExtractor; + /** * This annotation is used on class level * @@ -185,7 +187,7 @@ private Object getMessageObject(Set methods) { } private Message buildMessage(Method method) { - Class payloadType = getPayloadType(method); + Class payloadType = payloadClassExtractor.extractFrom(method); String modelName = schemasService.register(payloadType); String headerModelName = schemasService.register(this.buildHeaders(method)); diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractMethodLevelListenerScanner.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractMethodLevelListenerScanner.java index f58c3c20f..f7426fe12 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractMethodLevelListenerScanner.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/AbstractMethodLevelListenerScanner.java @@ -89,6 +89,7 @@ private List> mapToChannels(Set> compone * @param method The listener method. * @return The class object of the payload received by the listener. */ + // TODO: Inject SpringPayloadAnnotationTypeExtractor here with default implementation? protected abstract Class getPayloadType(Method method); private Set getAnnotatedMethods(Class type) { diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/SpringPayloadAnnotationTypeExtractor.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/SpringPayloadAnnotationTypeExtractor.java deleted file mode 100644 index bb13bf3e1..000000000 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/SpringPayloadAnnotationTypeExtractor.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.messaging.handler.annotation.Payload; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.List; - -@Slf4j -public class SpringPayloadAnnotationTypeExtractor { - public SpringPayloadAnnotationTypeExtractor() {} - - public static Class getPayloadType(Method method) { - String methodName = String.format("%s::%s", method.getDeclaringClass().getSimpleName(), method.getName()); - log.debug("Finding payload type for {}", methodName); - - Class[] parameterTypes = method.getParameterTypes(); - int parameterPayloadIndex = - getPayloadParameterIndex(parameterTypes, method.getParameterAnnotations(), methodName); - - return getPayloadParameterClass(method, parameterTypes, parameterPayloadIndex); - } - - static Class getPayloadParameterClass(Method method, Class[] parameterTypes, int parameterPayloadIndex) { - Class parameterClass = parameterTypes[parameterPayloadIndex]; - - try { - // Resolve generic type for batch listeners - if (parameterClass == List.class) { - Type type = ((ParameterizedType) method.getGenericParameterTypes()[parameterPayloadIndex]) - .getActualTypeArguments()[0]; - return Class.forName(type.getTypeName()); - } - } catch (Exception ex) { - log.info("Found payload type List, but was unable to extract generic data type", ex); - } - - return parameterClass; - } - - static int getPayloadParameterIndex( - Class[] parameterTypes, Annotation[][] parameterAnnotations, String methodName) { - switch (parameterTypes.length) { - case 0 -> throw new IllegalArgumentException("Listener methods must not have 0 parameters: " + methodName); - case 1 -> { - return 0; - } - default -> { - int payloadAnnotatedParameterIndex = getPayloadAnnotatedParameterIndex(parameterAnnotations); - if (payloadAnnotatedParameterIndex == -1) { - String msg = - "Multi-parameter AsyncListener methods must have one parameter annotated with @Payload, " - + "but none was found: " - + methodName; - - throw new IllegalArgumentException(msg); - } - return payloadAnnotatedParameterIndex; - } - } - } - - static int getPayloadAnnotatedParameterIndex(Annotation[][] parameterAnnotations) { - for (int i = 0, length = parameterAnnotations.length; i < length; i++) { - Annotation[] annotations = parameterAnnotations[i]; - boolean hasPayloadAnnotation = Arrays.stream(annotations).anyMatch(Payload.class::isInstance); - - if (hasPayloadAnnotation) { - return i; - } - } - - return -1; - } -} 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 b25b7765e..e2dd7a063 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 @@ -5,8 +5,8 @@ import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.MessageBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.SpringPayloadAnnotationTypeExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.AbstractOperationDataScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.ConsumerData; import io.github.stavshamir.springwolf.asyncapi.types.OperationData; @@ -40,8 +40,9 @@ public class AsyncListenerAnnotationScanner extends AbstractOperationDataScanner private final SchemasService schemasService; private final AsyncApiDocketService asyncApiDocketService; - private final List operationBindingProcessors; + private final PayloadClassExtractor payloadClassExtractor; + private final List operationBindingProcessors; private final List messageBindingProcessors; @Override @@ -108,10 +109,8 @@ private ConsumerData toConsumerData( Message message, AsyncListener annotation) { AsyncOperation op = annotation.operation(); - Class payloadType = op.payloadType() != Object.class - ? op.payloadType() - : SpringPayloadAnnotationTypeExtractor.getPayloadType(method); - + Class payloadType = + op.payloadType() != Object.class ? op.payloadType() : payloadClassExtractor.extractFrom(method); return ConsumerData.builder() .channelName(resolver.resolveStringValue(op.channelName())) .description(resolver.resolveStringValue(op.description())) 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 ebcdb6402..b545be54f 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 @@ -5,8 +5,8 @@ import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.MessageBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.OperationBindingProcessor; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.SpringPayloadAnnotationTypeExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.AbstractOperationDataScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.OperationData; import io.github.stavshamir.springwolf.asyncapi.types.ProducerData; @@ -40,8 +40,9 @@ public class AsyncPublisherAnnotationScanner extends AbstractOperationDataScanne private final SchemasService schemasService; private final AsyncApiDocketService asyncApiDocketService; - private final List operationBindingProcessors; + private final PayloadClassExtractor payloadClassExtractor; + private final List operationBindingProcessors; private final List messageBindingProcessors; @Override @@ -108,9 +109,8 @@ private ProducerData toConsumerData( Message message, AsyncPublisher annotation) { AsyncOperation op = annotation.operation(); - Class payloadType = op.payloadType() != Object.class - ? op.payloadType() - : SpringPayloadAnnotationTypeExtractor.getPayloadType(method); + Class payloadType = + op.payloadType() != Object.class ? op.payloadType() : payloadClassExtractor.extractFrom(method); return ProducerData.builder() .channelName(resolver.resolveStringValue(op.channelName())) .description(resolver.resolveStringValue(op.description())) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/payload/PayloadClassExtractor.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/payload/PayloadClassExtractor.java new file mode 100644 index 000000000..cec00240b --- /dev/null +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/payload/PayloadClassExtractor.java @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload; + +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.Payload; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Map; + +@Slf4j +public class PayloadClassExtractor { + private final TypeToClassConverter typeToClassConverter; + + public PayloadClassExtractor(SpringwolfConfigProperties properties) { + Map extractableClasses = Map.of(); + if (properties.getPayload() != null) { + extractableClasses = properties.getPayload().getExtractableClasses(); + } + typeToClassConverter = new TypeToClassConverter(extractableClasses); + } + + public Class extractFrom(Method method) { + String methodName = String.format("%s::%s", method.getDeclaringClass().getSimpleName(), method.getName()); + log.debug("Finding payload type for {}", methodName); + + int parameterPayloadIndex = + getPayloadParameterIndex(method.getParameterTypes(), method.getParameterAnnotations(), methodName); + + return typeToClassConverter.extractClass(method.getGenericParameterTypes()[parameterPayloadIndex]); + } + + public Class typeToClass(Type type) { + return typeToClassConverter.extractClass(type); + } + + private int getPayloadParameterIndex( + Class[] parameterClasses, Annotation[][] parameterAnnotations, String methodName) { + switch (parameterClasses.length) { + case 0 -> throw new IllegalArgumentException("Listener methods must not have 0 parameters: " + methodName); + case 1 -> { + return 0; + } + default -> { + int payloadAnnotatedParameterIndex = getPayloadAnnotatedParameterIndex(parameterAnnotations); + if (payloadAnnotatedParameterIndex == -1) { + String msg = + "Multi-parameter AsyncListener methods must have one parameter annotated with @Payload, " + + "but none was found: " + + methodName; + + throw new IllegalArgumentException(msg); + } + return payloadAnnotatedParameterIndex; + } + } + } + + private int getPayloadAnnotatedParameterIndex(Annotation[][] parameterAnnotations) { + for (int i = 0, length = parameterAnnotations.length; i < length; i++) { + Annotation[] annotations = parameterAnnotations[i]; + boolean hasPayloadAnnotation = Arrays.stream(annotations).anyMatch(Payload.class::isInstance); + + if (hasPayloadAnnotation) { + return i; + } + } + + return -1; + } + + @RequiredArgsConstructor + private static class TypeToClassConverter { + + private final Map extractableClassToArgumentIndex; + + private Class extractClass(Type parameterType) { + try { + if (parameterType instanceof ParameterizedType) { + Type rawParameterType = ((ParameterizedType) parameterType).getRawType(); + String rawParameterTypeName = rawParameterType.getTypeName(); + + Class actualPayloadClass = + extractActualGenericClass((ParameterizedType) parameterType, rawParameterTypeName); + if (actualPayloadClass != Void.class) { + return actualPayloadClass; + } + + // nested generic class - fallback to most outer container + return Class.forName(rawParameterTypeName); + } + + // no generics used - just a normal type + return Class.forName(parameterType.getTypeName()); + } catch (Exception ex) { + log.info("Unable to extract generic data type of %s".formatted(parameterType), ex); + } + return Void.class; + } + + private Class extractActualGenericClass(ParameterizedType parameterType, String rawParameterTypeName) { + Type type = parameterType; + String typeName = rawParameterTypeName; + + while (type instanceof ParameterizedType && extractableClassToArgumentIndex.containsKey(typeName)) { + Integer index = extractableClassToArgumentIndex.get(rawParameterTypeName); + + type = ((ParameterizedType) type).getActualTypeArguments()[index]; + + typeName = type.getTypeName(); + if (type instanceof ParameterizedType) { + typeName = ((ParameterizedType) type).getRawType().getTypeName(); + } + } + + try { + return Class.forName(typeName); + } catch (ClassNotFoundException ex) { + log.debug("Unable to find class for type %s".formatted(typeName), ex); + } + + return Void.class; + } + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java index 8cb676435..a6d60c9ae 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java @@ -12,6 +12,7 @@ import org.springframework.lang.Nullable; import java.util.Map; +import java.util.stream.Collectors; @ConfigurationProperties(prefix = SpringwolfConfigConstants.SPRINGWOLF_CONFIG_PREFIX) @Getter @@ -60,6 +61,9 @@ public enum InitMode { @Nullable private Scanner scanner; + @Nullable + private Payload payload = new Payload(); + @Getter @Setter public static class ConfigDocket { @@ -226,4 +230,30 @@ private static class Actuator { private boolean enabled = false; } } + + @Getter + public static class Payload { + /** + * In case the payload is wrapped, Springwolf will try to unwrap the specified generic classes. + * + * The format is: canonicalClassName=generic-argument-index + */ + private Map extractableClasses = Map.of( + "java.util.function.Consumer", + 0, + "java.util.function.Supplier", + 0, + "org.springframework.messaging.Message", + 0, + "org.apache.kafka.streams.kstream.KStream", + 1, + "java.util.List", + 0); + + public void setExtractableClasses(Map extractableClasses) { + this.extractableClasses = extractableClasses.entrySet().stream() + .filter(entry -> entry.getValue() >= 0) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + } } 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 891748eae..d91b612f5 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 @@ -19,7 +19,6 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; import lombok.Data; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -106,7 +105,7 @@ private AsyncAPI getAsyncAPITestObject() { } @Test - void AsyncAPI_should_map_to_a_valid_asyncapi_json() throws IOException, JSONException { + void AsyncAPI_should_map_to_a_valid_asyncapi_json() throws IOException { var asyncapi = getAsyncAPITestObject(); String actual = serializer.toJsonString(asyncapi); InputStream s = this.getClass().getResourceAsStream("/asyncapi/asyncapi.json"); @@ -115,7 +114,7 @@ void AsyncAPI_should_map_to_a_valid_asyncapi_json() throws IOException, JSONExce } @Test - void AsyncAPI_should_map_to_a_valid_asyncapi_yaml() throws IOException, JSONException { + void AsyncAPI_should_map_to_a_valid_asyncapi_yaml() throws IOException { var asyncapi = getAsyncAPITestObject(); String actual = serializer.toYaml(asyncapi); InputStream s = this.getClass().getResourceAsStream("/asyncapi/asyncapi.yaml"); 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 947e9ba22..cc7efe013 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 @@ -8,6 +8,7 @@ import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ConsumerOperationDataScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProducerOperationDataScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.types.AsyncAPI; import io.github.stavshamir.springwolf.asyncapi.types.ConsumerData; import io.github.stavshamir.springwolf.asyncapi.types.ProducerData; @@ -39,6 +40,7 @@ DefaultAsyncApiService.class, DefaultChannelsService.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, ProducerOperationDataScanner.class, ConsumerOperationDataScanner.class, diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/SpringPayloadAnnotationTypeExtractorTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/SpringPayloadAnnotationTypeExtractorTest.java deleted file mode 100644 index 86358d6d9..000000000 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/SpringPayloadAnnotationTypeExtractorTest.java +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; - -import org.junit.jupiter.api.Test; -import org.springframework.messaging.handler.annotation.Payload; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class SpringPayloadAnnotationTypeExtractorTest { - - @Test - void getPayloadType() throws NoSuchMethodException { - Method m = TestClass.class.getDeclaredMethod("consumeWithString", String.class); - - Class result = SpringPayloadAnnotationTypeExtractor.getPayloadType(m); - - assertEquals(String.class, result); - } - - @Test - void getPayloadTypeWithPayloadAnnotation() throws NoSuchMethodException { - Method m = - TestClass.class.getDeclaredMethod("consumeWithStringAndPayloadAnnotation", String.class, Integer.class); - - Class result = SpringPayloadAnnotationTypeExtractor.getPayloadType(m); - - assertEquals(String.class, result); - } - - @Test - void getPayloadTypeWithListOfStrings() throws NoSuchMethodException { - Method m = TestClass.class.getDeclaredMethod("consumeWithListOfStrings", List.class); - - Class result = SpringPayloadAnnotationTypeExtractor.getPayloadType(m); - - assertEquals(String.class, result); - } - - @Test - void getPayloadTypeWithListOfInterfaces() throws NoSuchMethodException { - Method m = TestClass.class.getDeclaredMethod("consumeWithListOfGenericClasses", List.class); - - Class result = SpringPayloadAnnotationTypeExtractor.getPayloadType(m); - - // Unable to resolve optional, fallback to root type list - assertEquals(List.class, result); - } - - @Test - void getPayloadTypeWithInterface() throws NoSuchMethodException { - Method m = TestClass.class.getDeclaredMethod("consumeWithGenericClass", Optional.class); - - Class result = SpringPayloadAnnotationTypeExtractor.getPayloadType(m); - - assertEquals(Optional.class, result); - } - - @Test - void getPayloadTypeWithListOfStringExtends() throws NoSuchMethodException { - Method m = TestClass.class.getDeclaredMethod("consumeWithListOfStringExtends", List.class); - - Class result = SpringPayloadAnnotationTypeExtractor.getPayloadType(m); - - // Unable to resolve optional, fallback to root type list - assertEquals(List.class, result); - } - - @Test - void getPayloadTypeWithCustomType() throws NoSuchMethodException { - Method m = TestClass.class.getDeclaredMethod("consumeWithCustomType", TestClass.MyType.class); - - Class result = SpringPayloadAnnotationTypeExtractor.getPayloadType(m); - - assertEquals(TestClass.MyType.class, result); - } - - public static class TestClass { - public void consumeWithStringAndPayloadAnnotation(@Payload String value, Integer value2) {} - - public void consumeWithString(String value) {} - - public void consumeWithGenericClass(Optional value) {} - - public void consumeWithListOfStrings(List value) {} - - public void consumeWithListOfGenericClasses(List> value) {} - - public void consumeWithListOfStringExtends(List value) {} - - public void consumeWithCustomType(MyType value) {} - - public static class MyType extends ArrayList {} - } -} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/TestMethodLevelListenerScannerIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/TestMethodLevelListenerScannerIntegrationTest.java index 096814955..f6c9efc7a 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/TestMethodLevelListenerScannerIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/TestMethodLevelListenerScannerIntegrationTest.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 io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; 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; @@ -39,6 +40,7 @@ classes = { TestMethodLevelListenerScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class }) 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 b6eff4e33..689be6566 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 @@ -8,6 +8,7 @@ import com.asyncapi.v2.binding.channel.kafka.KafkaChannelBinding; import com.asyncapi.v2.binding.message.kafka.KafkaMessageBinding; import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.types.ConsumerData; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -42,6 +43,7 @@ classes = { ConsumerOperationDataScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) 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 237220b1d..575bcb220 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 @@ -8,6 +8,7 @@ import com.asyncapi.v2.binding.channel.kafka.KafkaChannelBinding; import com.asyncapi.v2.binding.message.kafka.KafkaMessageBinding; import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.types.ProducerData; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -42,6 +43,7 @@ classes = { ProducerOperationDataScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) 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 29a3492f5..1a50d65bc 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 @@ -6,6 +6,7 @@ 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.channels.payload.PayloadClassExtractor; 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; @@ -51,6 +52,7 @@ classes = { AsyncListenerAnnotationScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, TestOperationBindingProcessor.class 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 59f0cdefd..80df83965 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 @@ -4,6 +4,7 @@ import com.asyncapi.v2._6_0.model.channel.ChannelItem; import com.asyncapi.v2._6_0.model.channel.operation.Operation; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.TestOperationBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -44,6 +45,7 @@ classes = { AsyncPublisherAnnotationScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, TestOperationBindingProcessor.class diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/payload/PayloadClassExtractorTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/payload/PayloadClassExtractorTest.java new file mode 100644 index 000000000..4468917d5 --- /dev/null +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/payload/PayloadClassExtractorTest.java @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload; + +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.support.GenericMessage; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PayloadClassExtractorTest { + + private final PayloadClassExtractor extractor; + + { + SpringwolfConfigProperties properties = new SpringwolfConfigProperties(); + properties.setPayload(new SpringwolfConfigProperties.Payload()); + extractor = new PayloadClassExtractor(properties); + } + + @Test + void getPayloadType() throws NoSuchMethodException { + Method m = TestClass.class.getDeclaredMethod("consumeWithString", String.class); + + Class result = extractor.extractFrom(m); + + assertEquals(String.class, result); + } + + @Test + void getPayloadTypeWithPayloadAnnotation() throws NoSuchMethodException { + Method m = + TestClass.class.getDeclaredMethod("consumeWithStringAndPayloadAnnotation", String.class, Integer.class); + + Class result = extractor.extractFrom(m); + + assertEquals(String.class, result); + } + + @Test + void getPayloadTypeWithMessageOfInterfaces() throws NoSuchMethodException { + Method m = TestClass.class.getDeclaredMethod("consumeWithMessageOfGenericClasses", Message.class); + + Class result = extractor.extractFrom(m); + + assertEquals(Optional.class, result); + } + + @Test + void getPayloadTypeWithInterface() throws NoSuchMethodException { + Method m = TestClass.class.getDeclaredMethod("consumeWithGenericClass", Optional.class); + + Class result = extractor.extractFrom(m); + + assertEquals(Optional.class, result); + } + + @Test + void getPayloadTypeWithMessageOfStringExtends() throws NoSuchMethodException { + Method m = TestClass.class.getDeclaredMethod("consumeWithMessageOfStringExtends", Message.class); + + Class result = extractor.extractFrom(m); + + // Unable to resolve optional, fallback to root type Message + assertEquals(Message.class, result); + } + + @Test + void getPayloadTypeWithMessageOfListOfString() throws NoSuchMethodException { + Method m = TestClass.class.getDeclaredMethod("consumeWithMessageOfListOfString", Message.class); + + Class result = extractor.extractFrom(m); + + assertEquals(String.class, result); + } + + @Test + void getPayloadTypeWithMessageOfString() throws NoSuchMethodException { + Method m = TestClass.class.getDeclaredMethod("consumeWithMessageOfString", Message.class); + + Class result = extractor.extractFrom(m); + + assertEquals(String.class, result); + } + + @Test + void getPayloadTypeWithCustomType() throws NoSuchMethodException { + Method m = TestClass.class.getDeclaredMethod("consumeWithCustomType", TestClass.MyType.class); + + Class result = extractor.extractFrom(m); + + assertEquals(TestClass.MyType.class, result); + } + + public static class TestClass { + public void consumeWithStringAndPayloadAnnotation(@Payload String value, Integer value2) {} + + public void consumeWithString(String value) {} + + public void consumeWithGenericClass(Optional value) {} + + public void consumeWithMessageOfGenericClasses(Message> value) {} + + public void consumeWithMessageOfStringExtends(Message value) {} + + public void consumeWithMessageOfListOfString(Message> value) {} + + public void consumeWithMessageOfString(Message value) {} + + public void consumeWithCustomType(MyType value) {} + + public static class MyType extends GenericMessage { + public MyType(String payload) { + super(payload); + } + } + } +} diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfConfigPropertiesIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfConfigPropertiesIntegrationTest.java new file mode 100644 index 000000000..3a67ca99a --- /dev/null +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfConfigPropertiesIntegrationTest.java @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.configuration; + +import com.asyncapi.v2._6_0.model.server.Server; +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SpringwolfConfigPropertiesIntegrationTest { + + @Nested + @ExtendWith(SpringExtension.class) + @EnableConfigurationProperties(SpringwolfConfigProperties.class) + @TestPropertySource( + properties = { + "springwolf.enabled=true", + "springwolf.docket.info.title=Info title was loaded from spring properties", + "springwolf.docket.info.version=1.0.0", + "springwolf.docket.info.extension-fields.x-api-name=api-name", + "springwolf.docket.base-package=io.github.stavshamir.springwolf.example", + "springwolf.docket.servers.test-protocol.protocol=test", + "springwolf.docket.servers.test-protocol.url=some-server:1234", + }) + static class TestSimplePropertiesIntegrationTest { + + @Autowired + private SpringwolfConfigProperties properties; + + @Test + void enabledTest() { + assertThat(properties.isEnabled()).isTrue(); + } + + @Test + void docketInfoTest() { + assertThat(properties.getDocket()).isNotNull(); + assertThat(properties.getDocket().getInfo().getTitle()) + .isEqualTo("Info title was loaded from spring properties"); + assertThat(properties.getDocket().getInfo().getExtensionFields().get("x-api-name")) + .isEqualTo("api-name"); + } + + @Test + void docketBasePackageTest() { + assertThat(properties.getDocket().getBasePackage()).isEqualTo("io.github.stavshamir.springwolf.example"); + } + + @Test + void docketServersTest() { + assertThat(properties.getDocket().getServers()) + .isEqualTo(Map.of( + "test-protocol", + Server.builder() + .protocol("test") + .url("some-server:1234") + .build())); + } + } + + @Nested + @ExtendWith(SpringExtension.class) + @EnableConfigurationProperties(SpringwolfConfigProperties.class) + @TestPropertySource( + properties = { + "springwolf.enabled=true", + "springwolf.docket.info.title=Info title was loaded from spring properties", + "springwolf.docket.info.version=1.0.0", + "springwolf.docket.info.extension-fields.x-api-name=api-name", + "springwolf.docket.base-package=io.github.stavshamir.springwolf.example", + "springwolf.docket.servers.test-protocol.protocol=test", + "springwolf.docket.servers.test-protocol.url=some-server:1234", + }) + static class PayloadWithoutCustomizingIntegrationTest { + + @Autowired + private SpringwolfConfigProperties properties; + + @Test + void payloadTest() { + Map actual = properties.getPayload().getExtractableClasses(); + + // default values + assertThat(actual).hasSize(5); + assertThat(actual).containsEntry("org.apache.kafka.streams.kstream.KStream", 1); + assertThat(actual).containsEntry("org.springframework.messaging.Message", 0); + assertThat(actual).containsEntry("java.util.function.Consumer", 0); + assertThat(actual).containsEntry("java.util.function.Supplier", 0); + assertThat(actual).containsEntry("java.util.List", 0); + } + } + + @Nested + @ExtendWith(SpringExtension.class) + @EnableConfigurationProperties(SpringwolfConfigProperties.class) + @TestPropertySource( + properties = { + "springwolf.enabled=true", + "springwolf.docket.info.title=Info title was loaded from spring properties", + "springwolf.docket.info.version=1.0.0", + "springwolf.docket.info.extension-fields.x-api-name=api-name", + "springwolf.docket.base-package=io.github.stavshamir.springwolf.example", + "springwolf.docket.servers.test-protocol.protocol=test", + "springwolf.docket.servers.test-protocol.url=some-server:1234", + "springwolf.payload.extractable-classes.my.custom.class=1" + }) + static class PayloadWithCustomizingIntegrationTest { + + @Autowired + private SpringwolfConfigProperties properties; + + @Test + void payloadCustomizedTest() { + Map actual = properties.getPayload().getExtractableClasses(); + + // default values + assertThat(actual).hasSize(6); + assertThat(actual).containsEntry("my.custom.class", 1); + // default values + assertThat(actual).containsEntry("org.apache.kafka.streams.kstream.KStream", 1); + assertThat(actual).containsEntry("org.springframework.messaging.Message", 0); + assertThat(actual).containsEntry("java.util.function.Consumer", 0); + assertThat(actual).containsEntry("java.util.function.Supplier", 0); + assertThat(actual).containsEntry("java.util.List", 0); + } + } + + @Nested + @ExtendWith(SpringExtension.class) + @EnableConfigurationProperties(SpringwolfConfigProperties.class) + @TestPropertySource( + properties = { + "springwolf.enabled=true", + "springwolf.docket.info.title=Info title was loaded from spring properties", + "springwolf.docket.info.version=1.0.0", + "springwolf.docket.info.extension-fields.x-api-name=api-name", + "springwolf.docket.base-package=io.github.stavshamir.springwolf.example", + "springwolf.docket.servers.test-protocol.protocol=test", + "springwolf.docket.servers.test-protocol.url=some-server:1234", + "springwolf.payload.extractable-classes.java.util.List=-1" + }) + static class PayloadDisabledIntegrationTest { + + @Autowired + private SpringwolfConfigProperties properties; + + @Test + void payloadDisabledTest() { + Map actual = properties.getPayload().getExtractableClasses(); + + assertThat(actual).hasSize(4); + // default values + assertThat(actual).containsEntry("org.apache.kafka.streams.kstream.KStream", 1); + assertThat(actual).containsEntry("org.springframework.messaging.Message", 0); + assertThat(actual).containsEntry("java.util.function.Consumer", 0); + assertThat(actual).containsEntry("java.util.function.Supplier", 0); + } + } +} diff --git a/springwolf-examples/springwolf-amqp-example/build.gradle b/springwolf-examples/springwolf-amqp-example/build.gradle index f5cdef724..c66d1e6eb 100644 --- a/springwolf-examples/springwolf-amqp-example/build.gradle +++ b/springwolf-examples/springwolf-amqp-example/build.gradle @@ -20,29 +20,29 @@ dependencies { implementation "org.springframework.amqp:spring-rabbit" implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" implementation "io.swagger.core.v3:swagger-annotations:${swaggerVersion}" - implementation "com.asyncapi:asyncapi-core:${asyncapiCoreVersion}" implementation "org.springframework.amqp:spring-amqp" implementation "org.springframework.boot:spring-boot-autoconfigure" implementation "org.springframework.boot:spring-boot" - implementation "org.springframework:spring-beans" implementation "org.springframework:spring-context" testRuntimeOnly "org.junit.jupiter:junit-jupiter:${junitJupiterVersion}" - testImplementation "com.vaadin.external.google:android-json:${androidJsonVersion}" - testImplementation "org.assertj:assertj-core:${assertjCoreVersion}" testImplementation "org.awaitility:awaitility:${awaitilityVersion}" testImplementation "org.mockito:mockito-core:${mockitoCoreVersion}" testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" testImplementation "org.springframework.boot:spring-boot-test" + testImplementation "org.springframework:spring-beans" testImplementation "org.springframework:spring-web" testImplementation "org.springframework:spring-test" testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}" testImplementation "org.testcontainers:junit-jupiter:${testcontainersVersion}" + + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testCompileOnly "org.projectlombok:lombok:${lombokVersion}" } docker { diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/stavshamir/springwolf/example/amqp/configuration/AsyncApiConfiguration.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/stavshamir/springwolf/example/amqp/configuration/AsyncApiConfiguration.java deleted file mode 100644 index 4f457250a..000000000 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/stavshamir/springwolf/example/amqp/configuration/AsyncApiConfiguration.java +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.example.amqp.configuration; - -import com.asyncapi.v2._6_0.model.info.Contact; -import com.asyncapi.v2._6_0.model.info.Info; -import com.asyncapi.v2._6_0.model.info.License; -import com.asyncapi.v2._6_0.model.server.Server; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; -import io.github.stavshamir.springwolf.asyncapi.types.AmqpConsumerData; -import io.github.stavshamir.springwolf.asyncapi.types.AmqpProducerData; -import io.github.stavshamir.springwolf.configuration.AsyncApiDocket; -import io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.Map; - -@Configuration -public class AsyncApiConfiguration { - - private final String amqpHost; - private final String amqpPort; - - public AsyncApiConfiguration( - @Value("${spring.rabbitmq.host}") String amqpHost, @Value("${spring.rabbitmq.port}") int amqpPort) { - this.amqpHost = amqpHost; - this.amqpPort = String.valueOf(amqpPort); - } - - /** - * This bean is only required if full control on the {@link AsyncApiDocket} is needed - *

- * By default, Springwolf uses the {@see Info} provided in the application.properties - * Consumers are detected when the @RabbitListener or {@link AsyncListener} annotation is used - * Producers are detected when the springwolf {@link AsyncPublisher} annotation is used - */ - @Bean - @ConditionalOnProperty(value = "customAsyncApiDocketBean", havingValue = "true", matchIfMissing = true) - public AsyncApiDocket asyncApiDocket() { - Info info = Info.builder() - .version("1.0.0") - .title("Springwolf example project - AMQP") - .contact(Contact.builder() - .name("springwolf") - .url("https://github.com/springwolf/springwolf-core") - .email("example@example.com") - .build()) - .description("Springwolf example project to demonstrate springwolfs abilities") - .license(License.builder().name("Apache License 2.0").build()) - .build(); - - // the builder for asyncapi info, contact and license doesn't support setting/adding extensions, so - // we add text extension explicitely - info.setExtensionFields(Map.of("x-api-audience", "company-internal")); - info.getContact().setExtensionFields(Map.of("x-phone", "+49 123 456789")); - info.getLicense().setExtensionFields(Map.of("x-desc", "some description")); - - Server amqp = Server.builder() - .protocol("amqp") - .url(String.format("%s:%s", amqpHost, amqpPort)) - .build(); - - AmqpProducerData exampleProducer = AmqpProducerData.amqpProducerDataBuilder() - .queueName("example-producer-channel") - .description("example-producer-channel-description") - .exchangeName("example-topic-exchange") - .routingKey("example-topic-routing-key") - .payloadType(AnotherPayloadDto.class) - .build(); - - AmqpConsumerData exampleManuallyDefinedConsumer = AmqpConsumerData.amqpConsumerDataBuilder() - .queueName("example-manual-consumer-channel") - .description("example-manual-consumer-channel-description") - .exchangeName("example-consumer-topic-exchange") - .routingKey("example-consumer-topic-routing-key") - .payloadType(AnotherPayloadDto.class) - .build(); - - return AsyncApiDocket.builder() - .basePackage("io.github.stavshamir.springwolf.example") - .info(info) - .server("amqp", amqp) - .producer(exampleProducer) - .consumer(exampleManuallyDefinedConsumer) - .build(); - } -} diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiSystemTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiSystemTest.java index 2a9679e32..964c5ee15 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiSystemTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiSystemTest.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.amqp; -import org.json.JSONException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; @@ -23,6 +23,7 @@ * While the assertion of this test is identical to ApiIntegrationTests, * the setup uses a full docker-compose context with a real kafka instance. */ +@Slf4j @Testcontainers // @Ignore("Uncomment this line if you have issues running this test on your local machine.") public class ApiSystemTest { @@ -46,7 +47,8 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) - .withEnv(ENV); + .withEnv(ENV) + .withLogConsumer(APP_NAME, l -> log.debug("APP: %s".formatted(l.getUtf8StringWithoutLineEnding()))); private String baseUrl() { String host = environment.getServiceHost(APP_NAME, APP_PORT); @@ -55,7 +57,7 @@ private String baseUrl() { } @Test - void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException, JSONException { + void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException { String url = baseUrl() + "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketBeanIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketBeanIntegrationTest.java deleted file mode 100644 index ea556874a..000000000 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketBeanIntegrationTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.example.amqp; - -import org.springframework.test.context.TestPropertySource; - -/** - * Api integrationtest based on a SpringBoot application that defines a custom Docket bean. This contains Info and - * Server Informations as well as some explicit Producer and Consumer definitions. - */ -@TestPropertySource(properties = {"customAsyncApiDocketBean=true"}) -public class ApiWithDocketBeanIntegrationTest extends BaseApiIntegrationTest { - - @Override - protected String getExpectedApiFileName() { - return "/asyncapi.json"; - } -} diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketFromEnvironmentIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketFromEnvironmentIntegrationTest.java deleted file mode 100644 index a562f65ed..000000000 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketFromEnvironmentIntegrationTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.example.amqp; - -import org.springframework.test.context.TestPropertySource; - -/** - * Api integrationtest based on a SpringBoot application that defines all info and server properties via - * spring environment (from application.properties). - */ -@TestPropertySource(properties = {"customAsyncApiDocketBean=false"}) -public class ApiWithDocketFromEnvironmentIntegrationTest extends BaseApiIntegrationTest { - - @Override - protected String getExpectedApiFileName() { - return "/asyncapi_withdocketfromenvironment.json"; - } -} diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/BaseApiIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/BaseApiIntegrationTest.java deleted file mode 100644 index b2cfaa72d..000000000 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/BaseApiIntegrationTest.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.example.amqp; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Api integrationtest base class defining a SpringBootTest and a test method which asserts the resulting asyncapi. - * Subclasses can customize this test with @TestPropertySources and custom expectation file names. - * @see ApiWithDocketBeanIntegrationTest - * @see ApiWithDocketFromEnvironmentIntegrationTest - */ -@SpringBootTest( - classes = {SpringwolfAmqpExampleApplication.class}, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public abstract class BaseApiIntegrationTest { - - @Autowired - private TestRestTemplate restTemplate; - - @Value("${server.port}") - public Integer serverPort; - - @Test - void asyncApiResourceArtifactTest() throws JSONException, IOException { - String url = "/springwolf/docs"; - String actual = restTemplate.getForObject(url, String.class); - Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actual); - - String expectedApiFileName = getExpectedApiFileName(); - InputStream s = this.getClass().getResourceAsStream(expectedApiFileName); - String expected = new String(s.readAllBytes(), StandardCharsets.UTF_8); - - assertEquals(expected, actual); - } - - protected abstract String getExpectedApiFileName(); -} diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/SpringContextIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/SpringContextIntegrationTest.java index 623324ada..7eb8e0ddd 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/SpringContextIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/SpringContextIntegrationTest.java @@ -14,34 +14,10 @@ public class SpringContextIntegrationTest { - @Nested - @SpringBootTest(classes = SpringwolfAmqpExampleApplication.class) - class AsyncApiDocketTest { - - @Autowired - private ApplicationContext context; - - @Autowired - private AsyncApiService asyncApiService; - - @Test - void testContextWithAsyncApiDocketBean() { - assertNotNull(context); - - assertThat(asyncApiService.getAsyncAPI()).isNotNull(); - } - - @Test - void testAllChannelsAreFound() { - assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(7); - } - } - @Nested @SpringBootTest(classes = SpringwolfAmqpExampleApplication.class) @TestPropertySource( properties = { - "customAsyncApiDocketBean=false", "springwolf.enabled=true", "springwolf.docket.info.title=Info title was loaded from spring properties", "springwolf.docket.info.version=1.0.0", @@ -66,7 +42,6 @@ void testContextWithApplicationProperties() { @Test void testAllChannelsAreFound() { - // 2 channels defined in the AsyncDocket are not found (7 - 2 = 5) assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(5); } } diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json index 1f1cdaf20..6c6d4ad90 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json @@ -74,88 +74,6 @@ } } }, - "example-manual-consumer-channel": { - "publish": { - "operationId": "example-manual-consumer-channel_publish", - "description": "example-manual-consumer-channel-description", - "bindings": { - "amqp": { - "cc": [ - "example-consumer-topic-routing-key" - ], - "bindingVersion": "0.2.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", - "title": "AnotherPayloadDto", - "description": "Another payload model", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - } - }, - "bindings": { - "amqp": { - "is": "routingKey", - "exchange": { - "name": "example-consumer-topic-exchange", - "vhost": "/" - }, - "bindingVersion": "0.2.0" - } - } - }, - "example-producer-channel": { - "subscribe": { - "operationId": "example-producer-channel_subscribe", - "description": "example-producer-channel-description", - "bindings": { - "amqp": { - "cc": [ - "example-topic-routing-key" - ], - "bindingVersion": "0.2.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", - "title": "AnotherPayloadDto", - "description": "Another payload model", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - } - }, - "bindings": { - "amqp": { - "is": "routingKey", - "exchange": { - "name": "example-topic-exchange", - "vhost": "/" - }, - "bindingVersion": "0.2.0" - } - } - }, "example-producer-channel-publisher": { "subscribe": { "operationId": "example-producer-channel-publisher_subscribe", diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi_withdocketfromenvironment.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi_withdocketfromenvironment.json deleted file mode 100644 index 6c6d4ad90..000000000 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi_withdocketfromenvironment.json +++ /dev/null @@ -1,354 +0,0 @@ -{ - "asyncapi": "2.6.0", - "info": { - "title": "Springwolf example project - AMQP", - "version": "1.0.0", - "description": "Springwolf example project to demonstrate springwolfs abilities", - "contact": { - "name": "springwolf", - "url": "https://github.com/springwolf/springwolf-core", - "email": "example@example.com", - "x-phone": "+49 123 456789" - }, - "license": { - "name": "Apache License 2.0", - "x-desc": "some description" - }, - "x-api-audience": "company-internal" - }, - "defaultContentType": "application/json", - "servers": { - "amqp": { - "url": "amqp:5672", - "protocol": "amqp" - } - }, - "channels": { - "another-queue": { - "publish": { - "operationId": "another-queue_publish_receiveAnotherPayload", - "description": "Auto-generated description", - "bindings": { - "amqp": { - "cc": [ - "another-queue" - ], - "bindingVersion": "0.2.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", - "title": "AnotherPayloadDto", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - } - }, - "bindings": { - "amqp": { - "is": "queue", - "exchange": { - "name": "", - "type": "direct", - "durable": true, - "autoDelete": false, - "vhost": "/" - }, - "queue": { - "name": "another-queue", - "durable": false, - "exclusive": false, - "autoDelete": false, - "vhost": "/" - }, - "bindingVersion": "0.2.0" - } - } - }, - "example-producer-channel-publisher": { - "subscribe": { - "operationId": "example-producer-channel-publisher_subscribe", - "description": "Custom, optional description defined in the AsyncPublisher annotation", - "bindings": { - "amqp": { - "expiration": 0, - "cc": [ ], - "priority": 0, - "deliveryMode": 0, - "mandatory": false, - "timestamp": false, - "ack": false, - "bindingVersion": "0.2.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", - "title": "AnotherPayloadDto", - "description": "Another payload model", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - } - } - }, - "example-queue": { - "publish": { - "operationId": "example-queue_publish_receiveExamplePayload", - "description": "Auto-generated description", - "bindings": { - "amqp": { - "cc": [ - "example-queue" - ], - "bindingVersion": "0.2.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto", - "title": "ExamplePayloadDto", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - } - }, - "bindings": { - "amqp": { - "is": "queue", - "exchange": { - "name": "", - "type": "direct", - "durable": true, - "autoDelete": false, - "vhost": "/" - }, - "queue": { - "name": "example-queue", - "durable": false, - "exclusive": false, - "autoDelete": false, - "vhost": "/" - }, - "bindingVersion": "0.2.0" - } - } - }, - "example-topic-routing-key": { - "publish": { - "operationId": "example-topic-routing-key_publish_bindingsExample", - "description": "Auto-generated description", - "bindings": { - "amqp": { - "cc": [ - "example-topic-routing-key" - ], - "bindingVersion": "0.2.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", - "title": "AnotherPayloadDto", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - } - }, - "bindings": { - "amqp": { - "is": "routingKey", - "exchange": { - "name": "example-bindings-exchange-name", - "type": "topic", - "durable": true, - "autoDelete": false, - "vhost": "/" - }, - "queue": { - "name": "example-bindings-queue", - "durable": false, - "exclusive": true, - "autoDelete": true, - "vhost": "/" - }, - "bindingVersion": "0.2.0" - } - } - }, - "multi-payload-queue": { - "publish": { - "operationId": "multi-payload-queue_publish_bindingsBeanExample", - "description": "Auto-generated description", - "bindings": { - "amqp": { - "cc": [ - "example-topic-routing-key" - ], - "bindingVersion": "0.2.0" - } - }, - "message": { - "oneOf": [ - { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", - "title": "AnotherPayloadDto", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - }, - { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto", - "title": "ExamplePayloadDto", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "amqp": { - "bindingVersion": "0.2.0" - } - } - } - ] - } - }, - "bindings": { - "amqp": { - "is": "routingKey", - "exchange": { - "name": "example-topic-exchange", - "type": "topic", - "durable": true, - "autoDelete": false, - "vhost": "/" - }, - "queue": { - "name": "multi-payload-queue", - "durable": true, - "exclusive": false, - "autoDelete": false, - "vhost": "/" - }, - "bindingVersion": "0.2.0" - } - } - } - }, - "components": { - "schemas": { - "HeadersNotDocumented": { - "type": "object", - "properties": { }, - "example": { } - }, - "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto": { - "required": [ - "example" - ], - "type": "object", - "properties": { - "example": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto" - }, - "foo": { - "type": "string", - "description": "Foo field", - "example": "bar" - } - }, - "description": "Another payload model", - "example": { - "example": { - "someEnum": "FOO2", - "someLong": 5, - "someString": "some string value" - }, - "foo": "bar" - } - }, - "io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto": { - "required": [ - "someEnum", - "someString" - ], - "type": "object", - "properties": { - "someEnum": { - "type": "string", - "description": "Some enum field", - "example": "FOO2", - "enum": [ - "FOO1", - "FOO2", - "FOO3" - ] - }, - "someLong": { - "type": "integer", - "description": "Some long field", - "format": "int64", - "example": 5 - }, - "someString": { - "type": "string", - "description": "Some string field", - "example": "some string value" - } - }, - "description": "Example payload model", - "example": { - "someEnum": "FOO2", - "someLong": 5, - "someString": "some string value" - } - } - } - }, - "tags": [ ] -} \ No newline at end of file diff --git a/springwolf-examples/springwolf-cloud-stream-example/build.gradle b/springwolf-examples/springwolf-cloud-stream-example/build.gradle index 1eca634eb..b8ad253e5 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/build.gradle +++ b/springwolf-examples/springwolf-cloud-stream-example/build.gradle @@ -18,7 +18,7 @@ dependencyManagement { } dependencies { - implementation project(":springwolf-core") + testImplementation project(":springwolf-core") runtimeOnly project(":springwolf-plugins:springwolf-cloud-stream") annotationProcessor project(":springwolf-plugins:springwolf-cloud-stream") @@ -27,28 +27,33 @@ dependencies { runtimeOnly "org.springframework.boot:spring-boot-starter-web" runtimeOnly "org.springframework.boot:spring-boot-starter-actuator" - implementation "com.asyncapi:asyncapi-core:${asyncapiCoreVersion}" implementation "org.apache.kafka:kafka-streams:${kafkaStreamsVersion}" implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" implementation "io.swagger.core.v3:swagger-annotations:${swaggerVersion}" implementation "org.springframework.boot:spring-boot-autoconfigure" implementation "org.springframework.boot:spring-boot" - implementation "org.springframework:spring-beans" implementation "org.springframework:spring-context" + compileOnly "org.projectlombok:lombok:${lombokVersion}" + + annotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" testRuntimeOnly "org.junit.jupiter:junit-jupiter:${junitJupiterVersion}" - testImplementation "com.vaadin.external.google:android-json:${androidJsonVersion}" testImplementation "org.assertj:assertj-core:${assertjCoreVersion}" testImplementation "org.springframework.boot:spring-boot-test" testImplementation "org.springframework:spring-test" + testImplementation "org.springframework:spring-beans" testImplementation "org.springframework:spring-web" testImplementation "org.springframework.kafka:spring-kafka-test" testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}" testImplementation "org.testcontainers:junit-jupiter:${testcontainersVersion}" + + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testCompileOnly "org.projectlombok:lombok:${lombokVersion}" } docker { @@ -66,8 +71,5 @@ docker { } test { - minHeapSize = "128m" // initial heap size - maxHeapSize = "1024m" // maximum heap size - dependsOn dockerBuildImage } diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/main/java/io/github/stavshamir/springwolf/example/cloudstream/configuration/AsyncApiConfiguration.java b/springwolf-examples/springwolf-cloud-stream-example/src/main/java/io/github/stavshamir/springwolf/example/cloudstream/configuration/AsyncApiConfiguration.java deleted file mode 100644 index fa2fe4420..000000000 --- a/springwolf-examples/springwolf-cloud-stream-example/src/main/java/io/github/stavshamir/springwolf/example/cloudstream/configuration/AsyncApiConfiguration.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.example.cloudstream.configuration; - -import com.asyncapi.v2._6_0.model.info.Contact; -import com.asyncapi.v2._6_0.model.info.Info; -import com.asyncapi.v2._6_0.model.info.License; -import com.asyncapi.v2._6_0.model.server.Server; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; -import io.github.stavshamir.springwolf.configuration.AsyncApiDocket; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AsyncApiConfiguration { - - private final String BOOTSTRAP_SERVERS; - - public AsyncApiConfiguration(@Value("${spring.kafka.bootstrap-servers}") String bootstrapServers) { - this.BOOTSTRAP_SERVERS = bootstrapServers; - } - - /** - * This bean is only required if full control on the {@link AsyncApiDocket} is needed - *

- * By default, Springwolf uses the {@see Info} provided in the application.properties - * Consumers are detected when the @KafkaListener or {@link AsyncListener} annotation is used - * Producers are detected when the springwolf {@link AsyncPublisher} annotation is used - */ - @Bean - @ConditionalOnProperty(value = "customAsyncApiDocketBean", havingValue = "true", matchIfMissing = true) - public AsyncApiDocket asyncApiDocket() { - Info info = Info.builder() - .version("1.0.0") - .title("Springwolf example project - CloudStream") - .contact(Contact.builder() - .name("springwolf") - .url("https://github.com/springwolf/springwolf-core") - .email("example@example.com") - .build()) - .description("Springwolf example project") - .license(License.builder().name("Apache License 2.0").build()) - .build(); - - return AsyncApiDocket.builder() - .basePackage("io.github.stavshamir.springwolf.example") - .info(info) - .server( - "kafka", - Server.builder() - .protocol("kafka") - .url(BOOTSTRAP_SERVERS) - .build()) - .build(); - } -} diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiIntegrationTest.java b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiIntegrationTest.java index 7102b2e11..7dddbeadf 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiIntegrationTest.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.cloudstream; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -21,28 +20,26 @@ @SpringBootTest( classes = {SpringwolfCloudstreamExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@EmbeddedKafka( - partitions = 1, - brokerProperties = {"listeners=PLAINTEXT://localhost:29092", "port=29092"}) +@EmbeddedKafka(partitions = 1) @DirtiesContext public class ApiIntegrationTest { @Autowired private TestRestTemplate restTemplate; - @Value("${server.port}") - public Integer serverPort; + @Value("${spring.kafka.bootstrap-servers}") + public String bootstrapServers; @Test - void asyncApiResourceArtifactTest() throws JSONException, IOException { + void asyncApiResourceArtifactTest() throws IOException { String url = "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actual); InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); String expectedWithoutServersKafkaUrlPatch = new String(s.readAllBytes(), StandardCharsets.UTF_8); - // When running with EmbeddedKafka, localhost is used as hostname - String expected = expectedWithoutServersKafkaUrlPatch.replace("kafka:29092", "127.0.0.1:29092"); + // When running with EmbeddedKafka, the kafka bootstrap server does run on random ports + String expected = expectedWithoutServersKafkaUrlPatch.replace("kafka:29092", bootstrapServers); assertEquals(expected, actual); } diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiSystemTest.java b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiSystemTest.java index 68b517625..5b875acdc 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiSystemTest.java +++ b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/ApiSystemTest.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.cloudstream; -import org.json.JSONException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; @@ -23,6 +23,7 @@ * While the assertion of this test is identical to ApiIntegrationTests, * the setup uses a full docker-compose context with a real kafka instance. */ +@Slf4j @Testcontainers // @Ignore("Uncomment this line if you have issues running this test on your local machine.") public class ApiSystemTest { @@ -46,7 +47,8 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) - .withEnv(ENV); + .withEnv(ENV) + .withLogConsumer(APP_NAME, l -> log.debug("APP: %s".formatted(l.getUtf8StringWithoutLineEnding()))); private String baseUrl() { String host = environment.getServiceHost(APP_NAME, APP_PORT); @@ -55,7 +57,7 @@ private String baseUrl() { } @Test - void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException, JSONException { + void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException { String url = baseUrl() + "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/SpringContextIntegrationTest.java b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/SpringContextIntegrationTest.java index f785958ea..c15dd9716 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/SpringContextIntegrationTest.java +++ b/springwolf-examples/springwolf-cloud-stream-example/src/test/java/io/github/stavshamir/springwolf/example/cloudstream/SpringContextIntegrationTest.java @@ -16,28 +16,6 @@ public class SpringContextIntegrationTest { - @SpringBootTest(classes = SpringwolfCloudstreamExampleApplication.class) - @EmbeddedKafka( - partitions = 1, - brokerProperties = {"listeners=PLAINTEXT://localhost:29092", "port=29092"}) - @Nested - @DirtiesContext - class AsyncApiDocketTest { - - @Autowired - private ApplicationContext context; - - @Autowired - private AsyncApiService asyncApiService; - - @Test - void testContextWithAsyncApiDocketBean() { - assertNotNull(context); - - assertThat(asyncApiService.getAsyncAPI()).isNotNull(); - } - } - @SpringBootTest(classes = SpringwolfCloudstreamExampleApplication.class) @EmbeddedKafka( partitions = 1, @@ -46,7 +24,6 @@ void testContextWithAsyncApiDocketBean() { @DirtiesContext @TestPropertySource( properties = { - "customAsyncApiDocketBean=false", "springwolf.enabled=true", "springwolf.docket.info.title=Info title was loaded from spring properties", "springwolf.docket.info.version=1.0.0", diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-cloud-stream-example/src/test/resources/asyncapi.json index 4b93d45d0..6213321c1 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-cloud-stream-example/src/test/resources/asyncapi.json @@ -1,9 +1,9 @@ { "asyncapi": "2.6.0", "info": { - "title": "Springwolf example project - CloudStream", + "title": "Springwolf example project - Cloud Stream", "version": "1.0.0", - "description": "Springwolf example project", + "description": "Springwolf example project to demonstrate springwolfs abilities", "contact": { "name": "springwolf", "url": "https://github.com/springwolf/springwolf-core", diff --git a/springwolf-examples/springwolf-kafka-example/build.gradle b/springwolf-examples/springwolf-kafka-example/build.gradle index dbb57c2b1..6b4b10e1c 100644 --- a/springwolf-examples/springwolf-kafka-example/build.gradle +++ b/springwolf-examples/springwolf-kafka-example/build.gradle @@ -23,7 +23,6 @@ dependencies { implementation "org.springframework:spring-beans" implementation "org.springframework:spring-context" - implementation "org.springframework:spring-web" implementation "org.springframework.boot:spring-boot" implementation "org.springframework.boot:spring-boot-autoconfigure" @@ -33,7 +32,6 @@ dependencies { implementation "org.springframework.security:spring-security-config" implementation "org.springframework.security:spring-security-web" - implementation "com.asyncapi:asyncapi-core:${asyncapiCoreVersion}" implementation "io.swagger.core.v3:swagger-annotations:${swaggerVersion}" implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" @@ -42,8 +40,6 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" - testImplementation "com.vaadin.external.google:android-json:${androidJsonVersion}" - testImplementation "org.apache.kafka:kafka-clients:${kafkaClientsVersion}@jar" testImplementation "org.assertj:assertj-core:${assertjCoreVersion}" testImplementation "org.awaitility:awaitility:${awaitilityVersion}" @@ -62,6 +58,9 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-actuator") permitTestUnusedDeclared("org.springframework.boot:spring-boot-starter-actuator") + + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testCompileOnly "org.projectlombok:lombok:${lombokVersion}" } docker { diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/kafka/configuration/AsyncApiConfiguration.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/kafka/configuration/AsyncApiConfiguration.java deleted file mode 100644 index 39c4d67c0..000000000 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/stavshamir/springwolf/example/kafka/configuration/AsyncApiConfiguration.java +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.example.kafka.configuration; - -import com.asyncapi.v2._6_0.model.info.Contact; -import com.asyncapi.v2._6_0.model.info.Info; -import com.asyncapi.v2._6_0.model.info.License; -import com.asyncapi.v2._6_0.model.server.Server; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; -import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; -import io.github.stavshamir.springwolf.asyncapi.types.KafkaConsumerData; -import io.github.stavshamir.springwolf.asyncapi.types.KafkaProducerData; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; -import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersForCloudEventsBuilder; -import io.github.stavshamir.springwolf.configuration.AsyncApiDocket; -import io.github.stavshamir.springwolf.example.kafka.dtos.AnotherPayloadDto; -import io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; - -import static io.github.stavshamir.springwolf.example.kafka.configuration.KafkaConfiguration.CONSUMER_TOPIC; -import static io.github.stavshamir.springwolf.example.kafka.configuration.KafkaConfiguration.PRODUCER_TOPIC; - -@Configuration -public class AsyncApiConfiguration { - - private final String BOOTSTRAP_SERVERS; - - public AsyncApiConfiguration(@Value("${spring.kafka.bootstrap-servers}") String bootstrapServers) { - this.BOOTSTRAP_SERVERS = bootstrapServers; - } - - /** - * This bean is only required if full control on the {@link AsyncApiDocket} is needed - *

- * By default, Springwolf uses the {@see Info} provided in the application.properties - * Consumers are detected when the @KafkaListener or {@link AsyncListener} annotation is used - * Producers are detected when the springwolf {@link AsyncPublisher} annotation is used - */ - @Bean - @ConditionalOnProperty(value = "customAsyncApiDocketBean", havingValue = "true", matchIfMissing = true) - public AsyncApiDocket asyncApiDocket() { - Info info = Info.builder() - .version("1.0.0") - .title("Springwolf example project - Kafka") - .contact(Contact.builder() - .name("springwolf") - .url("https://github.com/springwolf/springwolf-core") - .email("example@example.com") - .build()) - .description("Springwolf example project to demonstrate springwolfs abilities") - .license(License.builder().name("Apache License 2.0").build()) - .build(); - - KafkaProducerData anotherProducerData = KafkaProducerData.kafkaProducerDataBuilder() - .topicName(PRODUCER_TOPIC) - .description("Custom, optional description for this produced to topic") - .payloadType(AnotherPayloadDto.class) - .headers(createCloudEventHeaders()) - .build(); - - KafkaConsumerData manuallyConfiguredConsumer = KafkaConsumerData.kafkaConsumerDataBuilder() - .topicName(CONSUMER_TOPIC) - .description("Custom, optional description for this consumed topic") - .payloadType(ExamplePayloadDto.class) - .build(); - - return AsyncApiDocket.builder() - .basePackage("io.github.stavshamir.springwolf.example") - .info(info) - .server( - "kafka", - Server.builder() - .protocol("kafka") - .url(BOOTSTRAP_SERVERS) - .build()) - .producer(anotherProducerData) - .consumer(manuallyConfiguredConsumer) - .build(); - } - - private static AsyncHeaders createCloudEventHeaders() { - AsyncHeaders ceBaseHeaders = createCloudEventsBaseHeaders(); - - return new AsyncHeadersForCloudEventsBuilder("CloudEventHeadersForAnotherPayloadDtoEndpoint", ceBaseHeaders) - .withTypeHeader("io.github.stavshamir.springwolf.CloudEventHeadersForAnotherPayloadDtoEndpoint") - .withSourceHeader("springwolf-kafka-example/anotherPayloadDtoEndpoint") - .withSubjectHeader("Test Subject") - .build(); - } - - private static AsyncHeaders createCloudEventsBaseHeaders() { - return new AsyncHeadersForCloudEventsBuilder() - .withContentTypeHeader(MediaType.APPLICATION_JSON) - .withSpecVersionHeader("1.0") - .withIdHeader("1234-1234-1234") - .withTimeHeader("2015-07-20T15:49:04-07:00") - .build(); - } -} diff --git a/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties b/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties index 7f0882c0f..d69e67c8d 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties +++ b/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties @@ -36,6 +36,7 @@ springwolf.docket.info.contact.name=springwolf springwolf.docket.info.contact.email=example@example.com springwolf.docket.info.contact.url=https://github.com/springwolf/springwolf-core springwolf.docket.info.license.name=Apache License 2.0 +springwolf.payload.extractable-classes.java.util.List=0 springwolf.use-fqn=true # Springwolf kafka configuration diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationTest.java index 83df8ab7a..f11f309b0 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationTest.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.kafka; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -21,31 +20,26 @@ @SpringBootTest( classes = {SpringwolfKafkaExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@EmbeddedKafka( - partitions = 1, - brokerProperties = { - "listeners=PLAINTEXT://localhost:9092", - "port=9092", - }) +@EmbeddedKafka(partitions = 1) @DirtiesContext public class ApiIntegrationTest { @Autowired private TestRestTemplate restTemplate; - @Value("${server.port}") - public Integer serverPort; + @Value("${spring.kafka.bootstrap-servers}") + public String bootstrapServers; @Test - void asyncApiResourceArtifactTest() throws JSONException, IOException { + void asyncApiResourceArtifactTest() throws IOException { String url = "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actual); InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); String expectedWithoutServersKafkaUrlPatch = new String(s.readAllBytes(), StandardCharsets.UTF_8); - // When running with EmbeddedKafka, localhost is used as hostname - String expected = expectedWithoutServersKafkaUrlPatch.replace("kafka:29092", "127.0.0.1:9092"); + // When running with EmbeddedKafka, the kafka bootstrap server does run on random ports + String expected = expectedWithoutServersKafkaUrlPatch.replace("kafka:29092", bootstrapServers); assertEquals(expected, actual); } diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationWithActuatorIntegrationTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationWithActuatorIntegrationTest.java index e3092aba2..0bae6f7ff 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationWithActuatorIntegrationTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiIntegrationWithActuatorIntegrationTest.java @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.kafka; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; @@ -23,12 +23,7 @@ "springwolf.endpoint.actuator.enabled=true", "management.endpoints.web.exposure.include=springwolf" }) -@EmbeddedKafka( - partitions = 1, - brokerProperties = { - "listeners=PLAINTEXT://localhost:9092", - "port=9092", - }) +@EmbeddedKafka(partitions = 1) @DirtiesContext public class ApiIntegrationWithActuatorIntegrationTest { @@ -38,15 +33,18 @@ public class ApiIntegrationWithActuatorIntegrationTest { @LocalServerPort private int managementPort; + @Value("${spring.kafka.bootstrap-servers}") + public String bootstrapServers; + @Test - void asyncApiResourceArtifactTest() throws JSONException, IOException { + void asyncApiResourceArtifactTest() throws IOException { String url = "http://localhost:" + managementPort + "/actuator/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); String expectedWithoutServersKafkaUrlPatch = new String(s.readAllBytes(), StandardCharsets.UTF_8); - // When running with EmbeddedKafka, localhost is used as hostname - String expected = expectedWithoutServersKafkaUrlPatch.replace("kafka:29092", "127.0.0.1:9092"); + // When running with EmbeddedKafka, the kafka bootstrap server does run on random ports + String expected = expectedWithoutServersKafkaUrlPatch.replace("kafka:29092", bootstrapServers); assertEquals(expected, actual); } diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiSystemTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiSystemTest.java index e66d10cc6..d6cd8da94 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiSystemTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ApiSystemTest.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.kafka; -import org.json.JSONException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; @@ -23,6 +23,7 @@ * While the assertion of this test is identical to ApiIntegrationTests, * the setup uses a full docker-compose context with a real kafka instance. */ +@Slf4j @Testcontainers // @Ignore("Uncomment this line if you have issues running this test on your local machine.") public class ApiSystemTest { @@ -46,7 +47,8 @@ public class ApiSystemTest { @Container public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) - .withEnv(ENV); + .withEnv(ENV) + .withLogConsumer(APP_NAME, l -> log.debug("APP: %s".formatted(l.getUtf8StringWithoutLineEnding()))); private String baseUrl() { String host = environment.getServiceHost(APP_NAME, APP_PORT); @@ -55,7 +57,7 @@ private String baseUrl() { } @Test - void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException, JSONException { + void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException { String url = baseUrl() + "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java index b7ae9ff4d..fc46ec712 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/ProducerSystemTest.java @@ -12,9 +12,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -38,7 +39,7 @@ classes = {SpringwolfKafkaExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers -@DirtiesContext +@TestPropertySource(properties = {"spring.kafka.bootstrap-servers=localhost:9092"}) @TestMethodOrder(OrderAnnotation.class) // @Ignore("Uncomment this line if you have issues running this test on your local machine.") public class ProducerSystemTest { @@ -60,7 +61,7 @@ public class ProducerSystemTest { @Order(1) void verifyKafkaIsAvailable() { Map consumerProperties = - properties.getPublishing().getProducer().buildProperties(); + properties.getPublishing().getProducer().buildProperties(new DefaultSslBundleRegistry()); AdminClient adminClient = KafkaAdminClient.create(consumerProperties); await().atMost(60, SECONDS) .untilAsserted( diff --git a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/SpringContextIntegrationTest.java b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/SpringContextIntegrationTest.java index b0e4b6ba8..67f22580d 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/SpringContextIntegrationTest.java +++ b/springwolf-examples/springwolf-kafka-example/src/test/java/io/github/stavshamir/springwolf/example/kafka/SpringContextIntegrationTest.java @@ -16,33 +16,6 @@ public class SpringContextIntegrationTest { - @SpringBootTest(classes = SpringwolfKafkaExampleApplication.class) - @EmbeddedKafka( - partitions = 1, - brokerProperties = {"listeners=PLAINTEXT://localhost:9092", "port=9092"}) - @Nested - @DirtiesContext - class AsyncApiDocketTest { - - @Autowired - private ApplicationContext context; - - @Autowired - private AsyncApiService asyncApiService; - - @Test - void testContextWithAsyncApiDocketBean() { - assertNotNull(context); - - assertThat(asyncApiService.getAsyncAPI()).isNotNull(); - } - - @Test - void testAllChannelsAreFound() { - assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(6); - } - } - @SpringBootTest(classes = SpringwolfKafkaExampleApplication.class) @EmbeddedKafka( partitions = 1, @@ -51,7 +24,6 @@ void testAllChannelsAreFound() { @DirtiesContext @TestPropertySource( properties = { - "customAsyncApiDocketBean=false", "springwolf.enabled=true", "springwolf.docket.info.title=Info title was loaded from spring properties", "springwolf.docket.info.version=1.0.0", @@ -76,8 +48,6 @@ void testContextWithApplicationProperties() { @Test void testAllChannelsAreFound() { - // 2 channels defined in the AsyncDocket are not found, - // however PRODUCER_TOPIC is also used in ExampleProducer (5 - 2 + 1 = 4) assertThat(asyncApiService.getAsyncAPI().getChannels()).hasSize(4); } } diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json index d5529f6bb..fbdc9e901 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json @@ -59,72 +59,6 @@ } } }, - "example-consumer-topic": { - "publish": { - "operationId": "example-consumer-topic_publish", - "description": "Custom, optional description for this consumed topic", - "bindings": { - "kafka": { - "bindingVersion": "0.4.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto", - "title": "ExamplePayloadDto", - "description": "Example payload model", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/HeadersNotDocumented" - }, - "bindings": { - "kafka": { - "bindingVersion": "0.4.0" - } - } - } - }, - "bindings": { - "kafka": { - "bindingVersion": "0.4.0" - } - } - }, - "example-producer-topic": { - "subscribe": { - "operationId": "example-producer-topic_subscribe", - "description": "Custom, optional description for this produced to topic", - "bindings": { - "kafka": { - "bindingVersion": "0.4.0" - } - }, - "message": { - "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", - "name": "io.github.stavshamir.springwolf.example.kafka.dtos.AnotherPayloadDto", - "title": "AnotherPayloadDto", - "description": "Another payload model", - "payload": { - "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.kafka.dtos.AnotherPayloadDto" - }, - "headers": { - "$ref": "#/components/schemas/CloudEventHeadersForAnotherPayloadDtoEndpoint" - }, - "bindings": { - "kafka": { - "bindingVersion": "0.4.0" - } - } - } - }, - "bindings": { - "kafka": { - "bindingVersion": "0.4.0" - } - } - }, "example-topic": { "publish": { "operationId": "example-topic_publish_receiveExamplePayload", @@ -279,132 +213,6 @@ }, "components": { "schemas": { - "CloudEventHeadersForAnotherPayloadDtoEndpoint": { - "type": "object", - "properties": { - "ce_id": { - "type": "string", - "description": "CloudEvent Id Header", - "example": "1234-1234-1234", - "enum": [ - "1234-1234-1234" - ] - }, - "ce_source": { - "type": "string", - "description": "CloudEvent Source Header", - "example": "springwolf-kafka-example/anotherPayloadDtoEndpoint", - "enum": [ - "springwolf-kafka-example/anotherPayloadDtoEndpoint" - ] - }, - "ce_specversion": { - "type": "string", - "description": "CloudEvent Spec Version Header", - "example": "1.0", - "enum": [ - "1.0" - ] - }, - "ce_subject": { - "type": "string", - "description": "CloudEvent Subject Header", - "example": "Test Subject", - "enum": [ - "Test Subject" - ] - }, - "ce_time": { - "type": "string", - "description": "CloudEvent Time Header", - "example": "2015-07-20T15:49:04-07:00", - "enum": [ - "2015-07-20T15:49:04-07:00" - ] - }, - "ce_type": { - "type": "string", - "description": "CloudEvent Payload Type Header", - "example": "io.github.stavshamir.springwolf.CloudEventHeadersForAnotherPayloadDtoEndpoint", - "enum": [ - "io.github.stavshamir.springwolf.CloudEventHeadersForAnotherPayloadDtoEndpoint" - ] - }, - "content-type": { - "type": "string", - "description": "CloudEvent Content-Type Header", - "example": "application/json", - "enum": [ - "application/json" - ] - } - }, - "example": { - "ce_id": "1234-1234-1234", - "ce_source": "springwolf-kafka-example/anotherPayloadDtoEndpoint", - "ce_specversion": "1.0", - "ce_subject": "Test Subject", - "ce_time": "2015-07-20T15:49:04-07:00", - "ce_type": "io.github.stavshamir.springwolf.CloudEventHeadersForAnotherPayloadDtoEndpoint", - "content-type": "application/json" - }, - "x-json-schema": { - "$schema": "https://json-schema.org/draft-04/schema#", - "name": "CloudEventHeadersForAnotherPayloadDtoEndpoint", - "properties": { - "ce_id": { - "description": "CloudEvent Id Header", - "enum": [ - "1234-1234-1234" - ], - "type": "string" - }, - "ce_source": { - "description": "CloudEvent Source Header", - "enum": [ - "springwolf-kafka-example/anotherPayloadDtoEndpoint" - ], - "type": "string" - }, - "ce_specversion": { - "description": "CloudEvent Spec Version Header", - "enum": [ - "1.0" - ], - "type": "string" - }, - "ce_subject": { - "description": "CloudEvent Subject Header", - "enum": [ - "Test Subject" - ], - "type": "string" - }, - "ce_time": { - "description": "CloudEvent Time Header", - "enum": [ - "2015-07-20T15:49:04-07:00" - ], - "type": "string" - }, - "ce_type": { - "description": "CloudEvent Payload Type Header", - "enum": [ - "io.github.stavshamir.springwolf.CloudEventHeadersForAnotherPayloadDtoEndpoint" - ], - "type": "string" - }, - "content-type": { - "description": "CloudEvent Content-Type Header", - "enum": [ - "application/json" - ], - "type": "string" - } - }, - "type": "object" - } - }, "HeadersNotDocumented": { "type": "object", "properties": { }, diff --git a/springwolf-examples/springwolf-sns-example/build.gradle b/springwolf-examples/springwolf-sns-example/build.gradle index 9851c78f1..3098e31b9 100644 --- a/springwolf-examples/springwolf-sns-example/build.gradle +++ b/springwolf-examples/springwolf-sns-example/build.gradle @@ -37,8 +37,6 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter:${junitJupiterVersion}" - testImplementation "com.vaadin.external.google:android-json:${androidJsonVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" testImplementation "org.springframework.boot:spring-boot-test" @@ -70,11 +68,4 @@ docker { test { dependsOn dockerBuildImage - dependsOn spotlessApply // Automatically fix code formatting if possible - - useJUnitPlatform() - - testLogging { - exceptionFormat = 'full' - } } diff --git a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationTest.java b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationTest.java index b3bc3eb12..345d5e8f3 100644 --- a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationTest.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.sns; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -37,7 +36,7 @@ static void setUpTestContainers(DynamicPropertyRegistry registry) { public Integer serverPort; @Test - void asyncApiResourceArtifactTest() throws JSONException, IOException { + void asyncApiResourceArtifactTest() throws IOException { String url = "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actual); diff --git a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiSystemTest.java b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiSystemTest.java index ff7c8f5cf..364dd22af 100644 --- a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiSystemTest.java +++ b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiSystemTest.java @@ -2,7 +2,6 @@ package io.github.stavshamir.springwolf.example.sns; import lombok.extern.slf4j.Slf4j; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; @@ -58,7 +57,7 @@ private String baseUrl() { } @Test - void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException, JSONException { + void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException { String url = baseUrl() + "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); diff --git a/springwolf-examples/springwolf-sqs-example/build.gradle b/springwolf-examples/springwolf-sqs-example/build.gradle index 3a35c5999..71ee479cd 100644 --- a/springwolf-examples/springwolf-sqs-example/build.gradle +++ b/springwolf-examples/springwolf-sqs-example/build.gradle @@ -36,8 +36,6 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter:${junitJupiterVersion}" - testImplementation "com.vaadin.external.google:android-json:${androidJsonVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" testImplementation "org.mockito:mockito-core:${mockitoCoreVersion}" @@ -50,6 +48,9 @@ dependencies { testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}" testImplementation "org.testcontainers:junit-jupiter:${testcontainersVersion}" testImplementation "org.testcontainers:localstack:${testcontainersVersion}" + + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testCompileOnly "org.projectlombok:lombok:${lombokVersion}" } docker { diff --git a/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiIntegrationTest.java b/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiIntegrationTest.java index a0a0545ad..e8f5c253e 100644 --- a/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiIntegrationTest.java @@ -1,11 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.sqs; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.test.context.DynamicPropertyRegistry; @@ -33,11 +31,8 @@ static void setUpTestContainers(DynamicPropertyRegistry registry) { @Autowired private TestRestTemplate restTemplate; - @Value("${server.port}") - public Integer serverPort; - @Test - void asyncApiResourceArtifactTest() throws JSONException, IOException { + void asyncApiResourceArtifactTest() throws IOException { String url = "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actual); diff --git a/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiSystemTest.java b/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiSystemTest.java index 7ffa816ca..9957883a5 100644 --- a/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiSystemTest.java +++ b/springwolf-examples/springwolf-sqs-example/src/test/java/io/github/stavshamir/springwolf/example/sqs/ApiSystemTest.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.sqs; -import org.json.JSONException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; @@ -23,6 +23,7 @@ * While the assertion of this test is identical to ApiIntegrationTests, * the setup uses a full docker-compose context with a real sqs instance. */ +@Slf4j @Testcontainers // @Ignore("Uncomment this line if you have issues running this test on your local machine.") public class ApiSystemTest { @@ -46,7 +47,8 @@ public class ApiSystemTest { @Container public DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) - .withEnv(ENV); + .withEnv(ENV) + .withLogConsumer(APP_NAME, l -> log.debug("APP: %s".formatted(l.getUtf8StringWithoutLineEnding()))); private String baseUrl() { String host = environment.getServiceHost(APP_NAME, APP_PORT); @@ -55,7 +57,7 @@ private String baseUrl() { } @Test - void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException, JSONException { + void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException { String url = baseUrl() + "/springwolf/docs"; String actual = restTemplate.getForObject(url, String.class); 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 eb2796b79..2eb93a76f 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,6 +7,7 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.ClassLevelRabbitListenerScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelRabbitListenerScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import org.springframework.amqp.core.Binding; @@ -37,10 +38,12 @@ public class SpringwolfAmqpScannerConfiguration { public ClassLevelRabbitListenerScanner classLevelRabbitListenerScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor, List queues, List exchanges, List bindings) { - return new ClassLevelRabbitListenerScanner(componentClassScanner, schemasService, queues, exchanges, bindings); + return new ClassLevelRabbitListenerScanner( + componentClassScanner, schemasService, payloadClassExtractor, queues, exchanges, bindings); } @Bean @@ -52,10 +55,12 @@ public ClassLevelRabbitListenerScanner classLevelRabbitListenerScanner( public MethodLevelRabbitListenerScanner methodLevelRabbitListenerScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor, List queues, List exchanges, List bindings) { - return new MethodLevelRabbitListenerScanner(componentClassScanner, schemasService, queues, exchanges, bindings); + return new MethodLevelRabbitListenerScanner( + componentClassScanner, schemasService, payloadClassExtractor, queues, exchanges, bindings); } @Bean diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScanner.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScanner.java index c60d37f5b..dcc6dff2e 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScanner.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScanner.java @@ -5,6 +5,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -31,10 +32,11 @@ public class ClassLevelRabbitListenerScanner extends AbstractClassLevelListenerS public ClassLevelRabbitListenerScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor, List queues, List exchanges, List bindings) { - super(componentClassScanner, schemasService); + super(componentClassScanner, schemasService, payloadClassExtractor); context = RabbitListenerUtil.RabbitListenerUtilContext.create(queues, exchanges, bindings); } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScanner.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScanner.java index 0c123acf0..6de3ca335 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScanner.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScanner.java @@ -5,6 +5,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import lombok.extern.slf4j.Slf4j; @@ -26,14 +27,18 @@ public class MethodLevelRabbitListenerScanner extends AbstractMethodLevelListene private final RabbitListenerUtil.RabbitListenerUtilContext context; private StringValueResolver resolver; + private final PayloadClassExtractor payloadClassExtractor; + public MethodLevelRabbitListenerScanner( ComponentClassScanner componentClassScanner, SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor, List queues, List exchanges, List bindings) { super(componentClassScanner, schemasService); - context = RabbitListenerUtil.RabbitListenerUtilContext.create(queues, exchanges, bindings); + this.context = RabbitListenerUtil.RabbitListenerUtilContext.create(queues, exchanges, bindings); + this.payloadClassExtractor = payloadClassExtractor; } @Override @@ -67,6 +72,6 @@ protected String getChannelName(RabbitListener annotation) { } protected Class getPayloadType(Method method) { - return SpringPayloadAnnotationTypeExtractor.getPayloadType(method); + return payloadClassExtractor.extractFrom(method); } } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScannerIntegrationTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScannerIntegrationTest.java index cf4f70343..fd85985f8 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelRabbitListenerScannerIntegrationTest.java @@ -7,6 +7,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.message.amqp.AMQPMessageBinding; import com.asyncapi.v2.binding.operation.amqp.AMQPOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -43,6 +44,7 @@ classes = { ClassLevelRabbitListenerScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) @@ -229,10 +231,10 @@ void scan_componentWithMultipleRabbitHandlerMethods() { } @Test - void scan_componentWithSingleRabbitHandlerMethod_batchPayload() { + void scan_componentWithSingleRabbitHandlerMethod_genericPayload() { // Given a @RabbitListener annotated class with one method annotated with @RabbitHandler - // - There is a payload of type List - setClassToScan(RabbitListenerClassWithRabbitHandlerWithBatchPayload.class); + // - There is a payload of type Message + setClassToScan(RabbitListenerClassWithRabbitHandlerWithGenericPayload.class); // When scan is called Map actualChannels = classLevelRabbitListenerScanner.scan(); @@ -248,7 +250,7 @@ void scan_componentWithSingleRabbitHandlerMethod_batchPayload() { Operation operation = Operation.builder() .description("Auto-generated description") - .operationId("RabbitListenerClassWithRabbitHandlerWithBatchPayload_publish") + .operationId("RabbitListenerClassWithRabbitHandlerWithGenericPayload_publish") .bindings(defaultOperationBinding) .message(message) .build(); @@ -293,10 +295,10 @@ private void anotherMethodWithoutAnnotation(SimpleBar payload) {} } @RabbitListener(queues = QUEUE) - private static class RabbitListenerClassWithRabbitHandlerWithBatchPayload { + private static class RabbitListenerClassWithRabbitHandlerWithGenericPayload { @RabbitHandler - private void methodWithAnnotation(List batchPayload) {} + private void methodWithAnnotation(org.springframework.messaging.Message payload) {} } @Data diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScannerIntegrationTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScannerIntegrationTest.java index 9ba91059d..0f6c9c0b3 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelRabbitListenerScannerIntegrationTest.java @@ -9,6 +9,7 @@ import com.asyncapi.v2.binding.message.amqp.AMQPMessageBinding; import com.asyncapi.v2.binding.operation.amqp.AMQPOperationBinding; import io.github.stavshamir.springwolf.asyncapi.MessageHelper; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -53,6 +54,7 @@ classes = { MethodLevelRabbitListenerScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, MethodLevelRabbitListenerScannerIntegrationTest.ClassWithRabbitListenerAnnotationsBindingBean.class diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfAmqpProducerConfigurationIntegrationTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfAmqpProducerConfigurationIntegrationTest.java index be3d53b0b..f733eedac 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfAmqpProducerConfigurationIntegrationTest.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfAmqpProducerConfigurationIntegrationTest.java @@ -5,6 +5,7 @@ import io.github.stavshamir.springwolf.asyncapi.amqp.SpringwolfAmqpAutoConfiguration; import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfAmqpController; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.producer.SpringwolfAmqpProducer; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -48,6 +49,7 @@ public class SpringwolfAmqpProducerConfigurationIntegrationTest { @MockBean(RabbitTemplate.class), @MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(AsyncApiDocketService.class) }) @Nested @@ -87,7 +89,8 @@ void springwolfAmqpProducerShouldBePresentInSpringContext() { @MockBean(AsyncApiService.class), @MockBean(RabbitTemplate.class), @MockBean(ComponentClassScanner.class), - @MockBean(SchemasService.class) + @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class), }) @Nested class AmqpProducerWillNotBeCreatedIfDisabledTest { diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/build.gradle b/springwolf-plugins/springwolf-cloud-stream-plugin/build.gradle index c221165be..6745d37ef 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/build.gradle +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/build.gradle @@ -41,6 +41,7 @@ dependencies { testImplementation "org.springframework.boot:spring-boot-test" testImplementation "org.springframework:spring-beans" testImplementation "org.springframework:spring-test" + testImplementation "org.springframework:spring-messaging" } jar { 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 3bcf729a4..739b38e64 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,8 @@ 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.FunctionalChannelBeanBuilder; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService; import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -23,8 +25,18 @@ public CloudStreamFunctionChannelsScanner cloudStreamFunctionChannelsScanner( AsyncApiDocketService asyncApiDocketService, BeanMethodsScanner beanMethodsScanner, SchemasService schemasService, - BindingServiceProperties cloudstreamBindingServiceProperties) { + BindingServiceProperties cloudstreamBindingServiceProperties, + FunctionalChannelBeanBuilder functionalChannelBeanBuilder) { return new CloudStreamFunctionChannelsScanner( - asyncApiDocketService, beanMethodsScanner, schemasService, cloudstreamBindingServiceProperties); + 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 81ebc2a63..acd309513 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 @@ -35,12 +35,13 @@ public class CloudStreamFunctionChannelsScanner implements ChannelsScanner { 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 ChannelMerger.merge(beanMethods.stream() - .map(FunctionalChannelBeanData::fromMethodBean) + .map(functionalChannelBeanBuilder::fromMethodBean) .flatMap(Set::stream) .filter(this::isChannelBean) .map(this::toChannelEntry) @@ -48,13 +49,13 @@ public Map scan() { } private boolean isChannelBean(FunctionalChannelBeanData beanData) { - return cloudStreamBindingsProperties.getBindings().containsKey(beanData.getCloudStreamBinding()); + return cloudStreamBindingsProperties.getBindings().containsKey(beanData.cloudStreamBinding()); } private Map.Entry toChannelEntry(FunctionalChannelBeanData beanData) { String channelName = cloudStreamBindingsProperties .getBindings() - .get(beanData.getCloudStreamBinding()) + .get(beanData.cloudStreamBinding()) .getDestination(); String operationId = buildOperationId(beanData, channelName); @@ -64,7 +65,7 @@ private Map.Entry toChannelEntry(FunctionalChannelBeanData } private ChannelItem buildChannel(FunctionalChannelBeanData beanData, String operationId) { - Class payloadType = beanData.getPayloadType(); + Class payloadType = beanData.payloadType(); String modelName = schemasService.register(payloadType); String headerModelName = schemasService.register(AsyncHeaders.NOT_DOCUMENTED); @@ -85,7 +86,7 @@ private ChannelItem buildChannel(FunctionalChannelBeanData beanData, String oper .build(); Map channelBinding = buildChannelBinding(); - return beanData.getBeanType() == FunctionalChannelBeanData.BeanType.CONSUMER + return beanData.beanType() == FunctionalChannelBeanData.BeanType.CONSUMER ? ChannelItem.builder() .bindings(channelBinding) .publish(operation) @@ -128,8 +129,8 @@ private String getProtocolName() { private String buildOperationId(FunctionalChannelBeanData beanData, String channelName) { String operationName = - beanData.getBeanType() == FunctionalChannelBeanData.BeanType.CONSUMER ? "publish" : "subscribe"; + beanData.beanType() == FunctionalChannelBeanData.BeanType.CONSUMER ? "publish" : "subscribe"; - return String.format("%s_%s_%s", channelName, operationName, beanData.getBeanName()); + 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/FunctionalChannelBeanBuilder.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanBuilder.java new file mode 100644 index 000000000..3281bc875 --- /dev/null +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanBuilder.java @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream; + +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import lombok.RequiredArgsConstructor; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.stream.Collectors.toList; + +@RequiredArgsConstructor +public class FunctionalChannelBeanBuilder { + private final PayloadClassExtractor extractor; + + public Set fromMethodBean(Method methodBean) { + Class returnType = methodBean.getReturnType(); + + if (Consumer.class.isAssignableFrom(returnType)) { + Class payloadType = getReturnTypeGenerics(methodBean).get(0); + return Set.of(ofConsumer(methodBean.getName(), payloadType)); + } + + if (Supplier.class.isAssignableFrom(returnType)) { + Class payloadType = getReturnTypeGenerics(methodBean).get(0); + return Set.of(ofSupplier(methodBean.getName(), payloadType)); + } + + if (Function.class.isAssignableFrom(returnType)) { + Class inputType = getReturnTypeGenerics(methodBean).get(0); + Class outputType = getReturnTypeGenerics(methodBean).get(1); + + return Set.of(ofConsumer(methodBean.getName(), inputType), ofSupplier(methodBean.getName(), outputType)); + } + + return Collections.emptySet(); + } + + private static FunctionalChannelBeanData ofConsumer(String name, Class payloadType) { + return new FunctionalChannelBeanData( + name, payloadType, FunctionalChannelBeanData.BeanType.CONSUMER, name + "-in-0"); + } + + private static FunctionalChannelBeanData ofSupplier(String name, Class payloadType) { + return new FunctionalChannelBeanData( + name, payloadType, FunctionalChannelBeanData.BeanType.SUPPLIER, name + "-out-0"); + } + + private List> getReturnTypeGenerics(Method methodBean) { + ParameterizedType genericReturnType = (ParameterizedType) methodBean.getGenericReturnType(); + return Arrays.stream(genericReturnType.getActualTypeArguments()) + .map(extractor::typeToClass) + .collect(toList()); + } +} diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanData.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanData.java index 747b277a9..0992af715 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanData.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanData.java @@ -1,91 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream; -import lombok.Data; - -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import static java.util.stream.Collectors.toList; - -@Data -class FunctionalChannelBeanData { - - private final String beanName; - private final Class payloadType; - private final BeanType beanType; - private final String cloudStreamBinding; - - static Set fromMethodBean(Method methodBean) { - Class returnType = methodBean.getReturnType(); - - if (Consumer.class.isAssignableFrom(returnType)) { - Class payloadType = getReturnTypeGenerics(methodBean).get(0); - return Set.of(ofConsumer(methodBean.getName(), payloadType)); - } - - if (Supplier.class.isAssignableFrom(returnType)) { - Class payloadType = getReturnTypeGenerics(methodBean).get(0); - return Set.of(ofSupplier(methodBean.getName(), payloadType)); - } - - if (Function.class.isAssignableFrom(returnType)) { - return fromFunctionBean(methodBean); - } - - return Collections.emptySet(); - } - - private static FunctionalChannelBeanData ofConsumer(String name, Class payloadType) { - return new FunctionalChannelBeanData(name, payloadType, BeanType.CONSUMER, name + "-in-0"); - } - - private static FunctionalChannelBeanData ofSupplier(String name, Class payloadType) { - return new FunctionalChannelBeanData(name, payloadType, BeanType.SUPPLIER, name + "-out-0"); - } - - private static Set fromFunctionBean(Method methodBean) { - String name = methodBean.getName(); - - Class inputType = getReturnTypeGenerics(methodBean).get(0); - Class outputType = getReturnTypeGenerics(methodBean).get(1); - - return Set.of(ofConsumer(name, inputType), ofSupplier(name, outputType)); - } - - private static List> getReturnTypeGenerics(Method methodBean) { - ParameterizedType genericReturnType = (ParameterizedType) methodBean.getGenericReturnType(); - return Arrays.stream(genericReturnType.getActualTypeArguments()) - .map(FunctionalChannelBeanData::toClassObject) - .collect(toList()); - } - - private static Class toClassObject(Type type) { - if (type instanceof Class) { - return (Class) type; - } - - if (type instanceof ParameterizedType) { - Type rawType = ((ParameterizedType) type).getRawType(); - - if ("org.apache.kafka.streams.kstream.KStream".equals(rawType.getTypeName())) { - return (Class) ((ParameterizedType) type).getActualTypeArguments()[1]; - } - - return (Class) rawType; - } - - throw new IllegalArgumentException( - "Cannot handle Type which is not Class or ParameterizedType, but was given: " + type.getClass()); - } +record FunctionalChannelBeanData(String beanName, Class payloadType, BeanType beanType, String cloudStreamBinding) { enum BeanType { CONSUMER, 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 9935d4cb0..7f0a2b7d5 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 @@ -6,6 +6,7 @@ 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.beans.DefaultBeanMethodsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ConfigurationClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.bindings.EmptyChannelBinding; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.bindings.EmptyOperationBinding; @@ -47,9 +48,11 @@ ConfigurationClassScanner.class, DefaultBeanMethodsScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, DefaultAsyncApiDocketService.class, CloudStreamFunctionChannelsScanner.class, + FunctionalChannelBeanBuilder.class, SpringwolfConfigProperties.class }) @Import(CloudStreamFunctionChannelsScannerIntegrationTest.Configuration.class) diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanBuilderTest.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanBuilderTest.java new file mode 100644 index 000000000..bdcbdd873 --- /dev/null +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanBuilderTest.java @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream; + +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; +import org.apache.kafka.streams.kstream.KStream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.Message; + +import java.lang.reflect.Method; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream.FunctionalChannelBeanData.BeanType.CONSUMER; +import static io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream.FunctionalChannelBeanData.BeanType.SUPPLIER; +import static org.assertj.core.api.Assertions.assertThat; + +class FunctionalChannelBeanBuilderTest { + private final SpringwolfConfigProperties properties = new SpringwolfConfigProperties(); + private final FunctionalChannelBeanBuilder functionalChannelBeanBuilder = + new FunctionalChannelBeanBuilder(new PayloadClassExtractor(properties)); + + @Nested + class NoBean { + @Test + void testNotAFunctionalChannelBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "notAFunctionalChannelBean"); + + Set data = functionalChannelBeanBuilder.fromMethodBean(method); + + assertThat(data).isEmpty(); + } + + @Bean + private String notAFunctionalChannelBean() { + return "foo"; + } + } + + @Nested + class consumerBean { + @Test + void testConsumerBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "consumerBean"); + + Set data = functionalChannelBeanBuilder.fromMethodBean(method); + + assertThat(data) + .containsExactly( + new FunctionalChannelBeanData("consumerBean", String.class, CONSUMER, "consumerBean-in-0")); + } + + @Bean + private Consumer consumerBean() { + return System.out::println; + } + } + + @Nested + class SupplierBean { + @Test + void testSupplierBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "supplierBean"); + + Set data = functionalChannelBeanBuilder.fromMethodBean(method); + + assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "supplierBean", String.class, SUPPLIER, "supplierBean-out-0")); + } + + @Bean + private Supplier supplierBean() { + return () -> "foo"; + } + } + + @Nested + class FunctionBean { + @Test + void testFunctionBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "functionBean"); + + Set data = functionalChannelBeanBuilder.fromMethodBean(method); + + assertThat(data) + .containsExactlyInAnyOrder( + new FunctionalChannelBeanData("functionBean", String.class, CONSUMER, "functionBean-in-0"), + new FunctionalChannelBeanData( + "functionBean", Integer.class, SUPPLIER, "functionBean-out-0")); + } + + @Bean + private Function functionBean() { + return s -> 1; + } + } + + @Nested + class ConsumerBeanWithGenericPayload { + + @Test + void testConsumerBeanWithGenericPayload() throws NoSuchMethodException { + String methodName = "consumerBeanWithGenericPayload"; + Method method = getMethod(this.getClass(), methodName); + + Set data = functionalChannelBeanBuilder.fromMethodBean(method); + + assertThat(data) + .containsExactly( + new FunctionalChannelBeanData(methodName, String.class, CONSUMER, methodName + "-in-0")); + } + + @Bean + private Consumer> consumerBeanWithGenericPayload() { + return System.out::println; + } + } + + @Nested + class KStreamBean { + + @Test + void testKafkaStreamsConsumerBean() throws NoSuchMethodException { + String methodName = "kafkaStreamsConsumerBean"; + Method method = getMethod(this.getClass(), methodName); + + Set data = functionalChannelBeanBuilder.fromMethodBean(method); + + assertThat(data) + .containsExactly( + new FunctionalChannelBeanData(methodName, String.class, CONSUMER, methodName + "-in-0")); + } + + @Bean + private Consumer> kafkaStreamsConsumerBean() { + return System.out::println; + } + } + + private static Method getMethod(Class clazz, String methodName) throws NoSuchMethodException { + return clazz.getDeclaredMethod(methodName); + } +} diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanDataTest.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanDataTest.java deleted file mode 100644 index a55266e17..000000000 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/cloudstream/FunctionalChannelBeanDataTest.java +++ /dev/null @@ -1,120 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream; - -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.Test; -import org.springframework.context.annotation.Bean; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import static io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream.FunctionalChannelBeanData.BeanType.CONSUMER; -import static io.github.stavshamir.springwolf.asyncapi.scanners.channels.cloudstream.FunctionalChannelBeanData.BeanType.SUPPLIER; -import static org.assertj.core.api.Assertions.assertThat; - -class FunctionalChannelBeanDataTest { - - @Test - void testNotAFunctionalChannelBean() throws NoSuchMethodException { - Method method = getMethod("notAFunctionalChannelBean"); - - Set data = FunctionalChannelBeanData.fromMethodBean(method); - - assertThat(data).isEmpty(); - } - - @Bean - private String notAFunctionalChannelBean() { - return "foo"; - } - - @Test - void testConsumerBean() throws NoSuchMethodException { - Method method = getMethod("consumerBean"); - - Set data = FunctionalChannelBeanData.fromMethodBean(method); - - assertThat(data) - .containsExactly( - new FunctionalChannelBeanData("consumerBean", String.class, CONSUMER, "consumerBean-in-0")); - } - - @Bean - private Consumer consumerBean() { - return System.out::println; - } - - @Test - void testSupplierBean() throws NoSuchMethodException { - Method method = getMethod("supplierBean"); - - Set data = FunctionalChannelBeanData.fromMethodBean(method); - - assertThat(data) - .containsExactly( - new FunctionalChannelBeanData("supplierBean", String.class, SUPPLIER, "supplierBean-out-0")); - } - - @Bean - private Supplier supplierBean() { - return () -> "foo"; - } - - @Test - void testFunctionBean() throws NoSuchMethodException { - Method method = getMethod("functionBean"); - - Set data = FunctionalChannelBeanData.fromMethodBean(method); - - assertThat(data) - .containsExactlyInAnyOrder( - new FunctionalChannelBeanData("functionBean", String.class, CONSUMER, "functionBean-in-0"), - new FunctionalChannelBeanData("functionBean", Integer.class, SUPPLIER, "functionBean-out-0")); - } - - @Bean - private Function functionBean() { - return s -> 1; - } - - @Test - void testConsumerBeanWithGenericPayload() throws NoSuchMethodException { - String methodName = "consumerBeanWithGenericPayload"; - Method method = getMethod(methodName); - - Set data = FunctionalChannelBeanData.fromMethodBean(method); - - assertThat(data) - .containsExactly(new FunctionalChannelBeanData(methodName, List.class, CONSUMER, methodName + "-in-0")); - } - - @Bean - private Consumer> consumerBeanWithGenericPayload() { - return System.out::println; - } - - @Test - void testKafkaStreamsConsumerBean() throws NoSuchMethodException { - String methodName = "kafkaStreamsConsumerBean"; - Method method = getMethod(methodName); - - Set data = FunctionalChannelBeanData.fromMethodBean(method); - - assertThat(data) - .containsExactly( - new FunctionalChannelBeanData(methodName, String.class, CONSUMER, methodName + "-in-0")); - } - - @Bean - private Consumer> kafkaStreamsConsumerBean() { - return System.out::println; - } - - private static Method getMethod(String methodName) throws NoSuchMethodException { - return FunctionalChannelBeanDataTest.class.getDeclaredMethod(methodName); - } -} 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 1767b15c8..2331b499d 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 @@ -6,6 +6,7 @@ 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.annotation.MethodLevelJmsListenerScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -26,8 +27,10 @@ public class SpringwolfJmsScannerConfiguration { @Order(value = ChannelPriority.AUTO_DISCOVERED) @ConditionalOnProperty(name = SPRINGWOLF_SCANNER_JMS_LISTENER_ENABLED, havingValue = "true", matchIfMissing = true) public MethodLevelJmsListenerScanner methodLevelJmsListenerScanner( - ComponentClassScanner componentClassScanner, SchemasService schemasService) { - return new MethodLevelJmsListenerScanner(componentClassScanner, schemasService); + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { + return new MethodLevelJmsListenerScanner(componentClassScanner, schemasService, payloadClassExtractor); } @Bean diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java index 1df17d1bb..1cdc795a2 100644 --- a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java @@ -5,6 +5,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import lombok.extern.slf4j.Slf4j; @@ -21,8 +22,15 @@ public class MethodLevelJmsListenerScanner extends AbstractMethodLevelListenerSc private StringValueResolver resolver; - public MethodLevelJmsListenerScanner(ComponentClassScanner componentClassScanner, SchemasService schemasService) { + private final PayloadClassExtractor payloadClassExtractor; + + public MethodLevelJmsListenerScanner( + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { super(componentClassScanner, schemasService); + + this.payloadClassExtractor = payloadClassExtractor; } @Override @@ -56,6 +64,6 @@ protected String getChannelName(JmsListener annotation) { } protected Class getPayloadType(Method method) { - return SpringPayloadAnnotationTypeExtractor.getPayloadType(method); + return payloadClassExtractor.extractFrom(method); } } diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java index f72a1208a..d57e4bffb 100644 --- a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java @@ -2,6 +2,7 @@ package io.github.stavshamir.springwolf.asyncapi.controller; import com.fasterxml.jackson.annotation.JsonProperty; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; import io.github.stavshamir.springwolf.producer.SpringwolfJmsProducer; import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; @@ -41,6 +42,7 @@ SpringwolfJmsController.class, PublishingPayloadCreator.class, SpringwolfJmsProducer.class, + PayloadClassExtractor.class, DefaultSchemasService.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java index e86dcdefe..6d2fdaaf0 100644 --- a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java @@ -7,6 +7,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.message.jms.JMSMessageBinding; import com.asyncapi.v2.binding.operation.jms.JMSOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -41,6 +42,7 @@ classes = { MethodLevelJmsListenerScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) @@ -187,6 +189,39 @@ void scan_componentHasJmsListenerMethods_multipleParamsWithPayloadAnnotation() { assertThat(actualChannelItems).containsExactly(Map.entry(QUEUE, expectedChannelItem)); } + @Test + void scan_componentWithSingleRabbitHandlerMethod_genericPayload() { + // Given a @JmsListener annotated class with one method annotated with @RabbitHandler + // - There is a payload of type Message + setClassToScan(ClassWithJmsListenerAnnotationWithGenericPayload.class); + + // When scan is called + Map actualChannelItems = scanner.scan(); + + // Then the returned collection contains the channel, and the payload is the generic type of the list + Message message = Message.builder() + .name(SimpleFoo.class.getName()) + .title(SimpleFoo.class.getSimpleName()) + .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) + .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) + .bindings(defaultMessageBinding) + .build(); + + Operation operation = Operation.builder() + .description("Auto-generated description") + .operationId("test-queue_publish_methodWithAnnotation") + .bindings(defaultOperationBinding) + .message(message) + .build(); + + ChannelItem expectedChannelItem = ChannelItem.builder() + .bindings(defaultChannelBinding) + .publish(operation) + .build(); + + assertThat(actualChannelItems).containsExactly(Map.entry(QUEUE, expectedChannelItem)); + } + private static class ClassWithoutJmsListenerAnnotations { private void methodWithoutAnnotation() {} @@ -218,6 +253,12 @@ private static class ClassWithJmsListenerAnnotationMultipleParamsWithPayloadAnno private void methodWithAnnotation(String anotherParam, @Payload SimpleFoo payload) {} } + private static class ClassWithJmsListenerAnnotationWithGenericPayload { + + @JmsListener(destination = QUEUE) + private void methodWithAnnotation(org.springframework.messaging.Message payload) {} + } + @Data @NoArgsConstructor private static class SimpleFoo { diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java index 13ea12c4e..b6dfc8077 100644 --- a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java @@ -6,6 +6,7 @@ import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfJmsController; import io.github.stavshamir.springwolf.asyncapi.jms.SpringwolfJmsAutoConfiguration; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.producer.SpringwolfJmsProducer; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -49,6 +50,7 @@ public class SpringwolfJmsProducerConfigurationIntegrationTest { @MockBean(SchemasService.class), @MockBean(AsyncApiDocketService.class), @MockBean(AsyncApiService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(JmsTemplate.class) }) @Nested @@ -88,6 +90,7 @@ void springwolfJmsProducerShouldBePresentInSpringContext() { @MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class), @MockBean(ChannelsService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(JmsTemplate.class) }) @Nested 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 a56417764..148685566 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,6 +7,7 @@ import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.ClassLevelKafkaListenerScanner; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelKafkaListenerScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -30,8 +31,10 @@ public class SpringwolfKafkaScannerConfiguration { havingValue = "true", matchIfMissing = true) public ClassLevelKafkaListenerScanner classLevelKafkaListenerScanner( - ComponentClassScanner componentClassScanner, SchemasService schemasService) { - return new ClassLevelKafkaListenerScanner(componentClassScanner, schemasService); + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { + return new ClassLevelKafkaListenerScanner(componentClassScanner, schemasService, payloadClassExtractor); } @Bean @@ -41,8 +44,10 @@ public ClassLevelKafkaListenerScanner classLevelKafkaListenerScanner( havingValue = "true", matchIfMissing = true) public MethodLevelKafkaListenerScanner methodLevelKafkaListenerScanner( - ComponentClassScanner componentClassScanner, SchemasService schemasService) { - return new MethodLevelKafkaListenerScanner(componentClassScanner, schemasService); + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { + return new MethodLevelKafkaListenerScanner(componentClassScanner, schemasService, payloadClassExtractor); } @Bean diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScanner.java b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScanner.java index 82386b774..839495689 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScanner.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScanner.java @@ -5,6 +5,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeadersForSpringKafkaBuilder; @@ -18,16 +19,17 @@ import java.lang.reflect.Method; import java.util.Map; -import static io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.SpringPayloadAnnotationTypeExtractor.getPayloadType; - @Slf4j public class ClassLevelKafkaListenerScanner extends AbstractClassLevelListenerScanner implements ChannelsScanner, EmbeddedValueResolverAware { private StringValueResolver resolver; - public ClassLevelKafkaListenerScanner(ComponentClassScanner componentClassScanner, SchemasService schemasService) { - super(componentClassScanner, schemasService); + public ClassLevelKafkaListenerScanner( + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { + super(componentClassScanner, schemasService, payloadClassExtractor); } @Override @@ -69,7 +71,7 @@ protected String getChannelName(KafkaListener annotation) { @Override protected AsyncHeaders buildHeaders(Method method) { - Class payloadType = getPayloadType(method); + Class payloadType = this.payloadClassExtractor.extractFrom(method); return new AsyncHeadersForSpringKafkaBuilder("SpringKafkaDefaultHeaders-" + payloadType.getSimpleName()) .withTypeIdHeader(payloadType.getTypeName()) .build(); diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScanner.java b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScanner.java index 7d9e0888b..8c6a1051f 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScanner.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScanner.java @@ -5,6 +5,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.operation.OperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import lombok.extern.slf4j.Slf4j; @@ -21,8 +22,14 @@ public class MethodLevelKafkaListenerScanner extends AbstractMethodLevelListener private StringValueResolver resolver; - public MethodLevelKafkaListenerScanner(ComponentClassScanner componentClassScanner, SchemasService schemasService) { + private final PayloadClassExtractor payloadClassExtractor; + + public MethodLevelKafkaListenerScanner( + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { super(componentClassScanner, schemasService); + this.payloadClassExtractor = payloadClassExtractor; } @Override @@ -59,6 +66,6 @@ protected String getChannelName(KafkaListener annotation) { @Override protected Class getPayloadType(Method method) { - return SpringPayloadAnnotationTypeExtractor.getPayloadType(method); + return payloadClassExtractor.extractFrom(method); } } diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactory.java b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactory.java index be700e1bc..98224d5af 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactory.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactory.java @@ -5,6 +5,7 @@ import io.github.stavshamir.springwolf.configuration.properties.SpringwolfKafkaConfigProperties; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.kafka.core.DefaultKafkaProducerFactory; import org.springframework.kafka.core.KafkaTemplate; @@ -25,7 +26,7 @@ public Optional> buildKafkaTemplate() { Map producerProperties = springWolfKafkaConfigProperties .getPublishing() .getProducer() - .buildProperties(); + .buildProperties(new DefaultSslBundleRegistry()); DefaultKafkaProducerFactory producerFactory = new DefaultKafkaProducerFactory<>(producerProperties); kafkaTemplate = Optional.of(new KafkaTemplate<>(producerFactory)); diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfKafkaControllerIntegrationTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfKafkaControllerIntegrationTest.java index 707bc049a..8081e70c8 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfKafkaControllerIntegrationTest.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfKafkaControllerIntegrationTest.java @@ -2,6 +2,7 @@ package io.github.stavshamir.springwolf.asyncapi.controller; import com.fasterxml.jackson.annotation.JsonProperty; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; import io.github.stavshamir.springwolf.producer.SpringwolfKafkaProducer; import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; @@ -42,6 +43,7 @@ PublishingPayloadCreator.class, SpringwolfKafkaProducer.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScannerIntegrationTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScannerIntegrationTest.java index 17f326c20..e91883f68 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/ClassLevelKafkaListenerScannerIntegrationTest.java @@ -7,6 +7,7 @@ import com.asyncapi.v2.binding.message.MessageBinding; import com.asyncapi.v2.binding.message.kafka.KafkaMessageBinding; import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -28,7 +29,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Comparator; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -43,6 +43,7 @@ classes = { ClassLevelKafkaListenerScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) @@ -211,10 +212,10 @@ void scan_componentWithMultipleKafkaHandlerMethods() { } @Test - void scan_componentWithSingleKafkaHandlerMethod_batchPayload() { + void scan_componentWithSingleKafkaHandlerMethod_genericPayload() { // Given a @KafkaListener annotated class with one method annotated with @KafkaHandler - // - There is a payload of type List - setClassToScan(KafkaListenerClassWithKafkaHandlerWithBatchPayload.class); + // - There is a payload of type Message + setClassToScan(KafkaListenerClassWithKafkaHandlerWithGenericPayload.class); // When scan is called Map actualChannels = classLevelKafkaListenerScanner.scan(); @@ -230,7 +231,7 @@ void scan_componentWithSingleKafkaHandlerMethod_batchPayload() { Operation operation = Operation.builder() .description("Auto-generated description") - .operationId("KafkaListenerClassWithKafkaHandlerWithBatchPayload_publish") + .operationId("KafkaListenerClassWithKafkaHandlerWithGenericPayload_publish") .bindings(defaultOperationBinding) .message(message) .build(); @@ -275,10 +276,10 @@ private void anotherMethodWithoutAnnotation(SimpleBar payload) {} } @KafkaListener(topics = TOPIC) - private static class KafkaListenerClassWithKafkaHandlerWithBatchPayload { + private static class KafkaListenerClassWithKafkaHandlerWithGenericPayload { @KafkaHandler - private void methodWithAnnotation(List batchPayload) {} + private void methodWithAnnotation(org.springframework.messaging.Message payload) {} } @Data diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScannerIntegrationTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScannerIntegrationTest.java index b8add57ed..dfb474e3e 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelKafkaListenerScannerIntegrationTest.java @@ -8,6 +8,7 @@ import com.asyncapi.v2.binding.message.kafka.KafkaMessageBinding; import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; import io.github.stavshamir.springwolf.asyncapi.MessageHelper; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -29,7 +30,6 @@ 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; @@ -43,6 +43,7 @@ classes = { MethodLevelKafkaListenerScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) @@ -272,9 +273,9 @@ void scan_componentHasKafkaListenerMethods_multiplePayloads() { } @Test - void scan_componentHasKafkaListenerMethods_batchPayload() { - // Given a class with a method annotated with KafkaListener with a payload of type List - setClassToScan(ClassWithKafkaListenerWithBatchPayload.class); + void scan_componentHasKafkaListenerMethods_genericPayload() { + // Given a class with a method annotated with KafkaListener with a payload of type Message + setClassToScan(ClassWithKafkaListenerWithGenericPayload.class); // When scan is called Map actualChannels = methodLevelKafkaListenerScanner.scan(); @@ -364,10 +365,10 @@ private void methodWithAnnotation(SimpleFoo payload) {} private void methodWithAnnotation(String payload) {} } - private static class ClassWithKafkaListenerWithBatchPayload { + private static class ClassWithKafkaListenerWithGenericPayload { @KafkaListener(topics = TOPIC) - private void methodWithAnnotation(List batchPayload) {} + private void methodWithAnnotation(org.springframework.messaging.Message payload) {} } @Data diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfKafkaProducerConfigurationIntegrationTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfKafkaProducerConfigurationIntegrationTest.java index d2c808533..838273b01 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfKafkaProducerConfigurationIntegrationTest.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfKafkaProducerConfigurationIntegrationTest.java @@ -4,6 +4,7 @@ import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfKafkaController; import io.github.stavshamir.springwolf.asyncapi.kafka.SpringwolfKafkaAutoConfiguration; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.producer.SpringwolfKafkaProducer; import io.github.stavshamir.springwolf.schemas.SchemasService; @@ -45,6 +46,7 @@ public class SpringwolfKafkaProducerConfigurationIntegrationTest { value = { @MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(AsyncApiDocketService.class) }) class KafkaProducerWillBeCreatedIfEnabledTest { @@ -79,7 +81,12 @@ void springwolfKafkaTemplateShouldBePresentInSpringContext() { "springwolf.docket.servers.test-protocol.url=some-server:1234", "springwolf.plugin.kafka.publishing.enabled=false" }) - @MockBeans(value = {@MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class)}) + @MockBeans( + value = { + @MockBean(ComponentClassScanner.class), + @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class) + }) class KafkaProducerWillNotBeCreatedIfDisabledTest { @Autowired private Optional springwolfKafkaProducer; diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactoryTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactoryTest.java index fadd4e448..374636681 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactoryTest.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfKafkaTemplateFactoryTest.java @@ -4,6 +4,7 @@ import io.github.stavshamir.springwolf.configuration.properties.SpringwolfKafkaConfigProperties; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.kafka.KafkaProperties; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.kafka.core.KafkaTemplate; import java.util.Map; @@ -53,6 +54,7 @@ void kafkaTemplateShouldBeCreatedWithProducerConfiguration() { Map configurationProperties = kafkaTemplate.get().getProducerFactory().getConfigurationProperties(); - assertThat(configurationProperties).isEqualTo(new KafkaProperties.Producer().buildProperties()); + assertThat(configurationProperties) + .isEqualTo(new KafkaProperties.Producer().buildProperties(new DefaultSslBundleRegistry())); } } diff --git a/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSnsProducerConfigurationIntegrationTest.java b/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSnsProducerConfigurationIntegrationTest.java index 17fb0a101..22ab0b72e 100644 --- a/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSnsProducerConfigurationIntegrationTest.java +++ b/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSnsProducerConfigurationIntegrationTest.java @@ -6,6 +6,7 @@ import io.github.stavshamir.springwolf.asyncapi.ChannelsService; import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfSnsController; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.sns.SpringwolfSnsAutoConfiguration; import io.github.stavshamir.springwolf.producer.SpringwolfSnsProducer; @@ -47,6 +48,7 @@ public class SpringwolfSnsProducerConfigurationIntegrationTest { value = { @MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(AsyncApiDocketService.class), @MockBean(AsyncApiService.class), @MockBean(SnsTemplate.class) @@ -87,6 +89,7 @@ void springwolfSqsProducerShouldBePresentInSpringContext() { value = { @MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(ChannelsService.class), @MockBean(SnsTemplate.class) }) diff --git a/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScanner.java b/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScanner.java index 74766bd90..344aba5d5 100644 --- a/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScanner.java +++ b/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScanner.java @@ -6,6 +6,7 @@ import com.asyncapi.v2.binding.operation.OperationBinding; import io.awspring.cloud.sqs.annotation.SqsListener; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import lombok.extern.slf4j.Slf4j; @@ -21,8 +22,14 @@ public class MethodLevelSqsListenerScanner extends AbstractMethodLevelListenerSc private StringValueResolver resolver; - public MethodLevelSqsListenerScanner(ComponentClassScanner componentClassScanner, SchemasService schemasService) { + private final PayloadClassExtractor payloadClassExtractor; + + public MethodLevelSqsListenerScanner( + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { super(componentClassScanner, schemasService); + this.payloadClassExtractor = payloadClassExtractor; } @Override @@ -56,6 +63,6 @@ protected String getChannelName(SqsListener annotation) { } protected Class getPayloadType(Method method) { - return SpringPayloadAnnotationTypeExtractor.getPayloadType(method); + return payloadClassExtractor.extractFrom(method); } } 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 ba566a411..838be07b9 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 @@ -6,6 +6,7 @@ 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.annotation.MethodLevelSqsListenerScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.schemas.SchemasService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -26,8 +27,10 @@ public class SpringwolfSqsScannerConfiguration { @Order(value = ChannelPriority.AUTO_DISCOVERED) @ConditionalOnProperty(name = SPRINGWOLF_SCANNER_SQS_LISTENER_ENABLED, havingValue = "true", matchIfMissing = true) public MethodLevelSqsListenerScanner methodLevelSqsListenerScanner( - ComponentClassScanner componentClassScanner, SchemasService schemasService) { - return new MethodLevelSqsListenerScanner(componentClassScanner, schemasService); + ComponentClassScanner componentClassScanner, + SchemasService schemasService, + PayloadClassExtractor payloadClassExtractor) { + return new MethodLevelSqsListenerScanner(componentClassScanner, schemasService, payloadClassExtractor); } @Bean diff --git a/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScannerIntegrationTest.java b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScannerIntegrationTest.java index ecc017256..907b5d05b 100644 --- a/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScannerIntegrationTest.java +++ b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelSqsListenerScannerIntegrationTest.java @@ -9,6 +9,7 @@ import com.asyncapi.v2.binding.operation.sqs.SQSOperationBinding; import io.awspring.cloud.sqs.annotation.SqsListener; import io.github.stavshamir.springwolf.asyncapi.MessageHelper; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; @@ -42,6 +43,7 @@ classes = { MethodLevelSqsListenerScanner.class, DefaultSchemasService.class, + PayloadClassExtractor.class, ExampleJsonGenerator.class, SpringwolfConfigProperties.class, }) diff --git a/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSqsProducerConfigurationIntegrationTest.java b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSqsProducerConfigurationIntegrationTest.java index 624b07acb..178c9849a 100644 --- a/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSqsProducerConfigurationIntegrationTest.java +++ b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfSqsProducerConfigurationIntegrationTest.java @@ -6,6 +6,7 @@ import io.github.stavshamir.springwolf.asyncapi.ChannelsService; import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfSqsController; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.payload.PayloadClassExtractor; import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; import io.github.stavshamir.springwolf.asyncapi.sqs.SpringwolfSqsAutoConfiguration; import io.github.stavshamir.springwolf.producer.SpringwolfSqsProducer; @@ -47,6 +48,7 @@ public class SpringwolfSqsProducerConfigurationIntegrationTest { value = { @MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(AsyncApiDocketService.class), @MockBean(AsyncApiService.class), @MockBean(SqsTemplate.class) @@ -87,6 +89,7 @@ void springwolfSqsProducerShouldBePresentInSpringContext() { value = { @MockBean(ComponentClassScanner.class), @MockBean(SchemasService.class), + @MockBean(PayloadClassExtractor.class), @MockBean(ChannelsService.class), @MockBean(SqsTemplate.class) })