diff --git a/springwolf-bindings/springwolf-googlepubsub-binding/src/main/java/io/github/springwolf/bindings/googlepubsub/scanners/channels/GooglePubSubChannelBindingProcessor.java b/springwolf-bindings/springwolf-googlepubsub-binding/src/main/java/io/github/springwolf/bindings/googlepubsub/scanners/channels/GooglePubSubChannelBindingProcessor.java index 56642a571..eafeb2e0d 100644 --- a/springwolf-bindings/springwolf-googlepubsub-binding/src/main/java/io/github/springwolf/bindings/googlepubsub/scanners/channels/GooglePubSubChannelBindingProcessor.java +++ b/springwolf-bindings/springwolf-googlepubsub-binding/src/main/java/io/github/springwolf/bindings/googlepubsub/scanners/channels/GooglePubSubChannelBindingProcessor.java @@ -11,7 +11,7 @@ import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.util.StringValueResolver; -import java.lang.reflect.Method; +import java.lang.reflect.AnnotatedElement; import java.util.Arrays; import java.util.Optional; @@ -24,8 +24,8 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) { } @Override - public Optional process(Method method) { - return Arrays.stream(method.getAnnotations()) + public Optional process(AnnotatedElement annotatedElement) { + return Arrays.stream(annotatedElement.getAnnotations()) .filter(GooglePubSubAsyncChannelBinding.class::isInstance) .map(GooglePubSubAsyncChannelBinding.class::cast) .findAny() diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/channels/ChannelBindingProcessor.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/channels/ChannelBindingProcessor.java index a79658a13..b0cbabcf7 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/channels/ChannelBindingProcessor.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/channels/ChannelBindingProcessor.java @@ -1,17 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.core.asyncapi.scanners.bindings.channels; -import java.lang.reflect.Method; +import java.lang.reflect.AnnotatedElement; import java.util.Optional; public interface ChannelBindingProcessor { /** - * Process the methods annotated with Channel Binding Annotation + * Process the elements annotated with Channel Binding Annotation * for protocol specific channelBinding annotations, method parameters, etc * - * @param method The method being annotated + * @param annotatedElement The element being annotated * @return A message binding, if found */ - Optional process(Method method); + Optional process(AnnotatedElement annotatedElement); } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/utils/AsyncAnnotationUtil.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/utils/AsyncAnnotationUtil.java index bfd1c1764..95ab67fb8 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/utils/AsyncAnnotationUtil.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/utils/AsyncAnnotationUtil.java @@ -20,6 +20,7 @@ import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; @@ -149,9 +150,9 @@ public static List getServers(AsyncOperation op, StringValueResolver res } public static Map processChannelBindingFromAnnotation( - Method method, List channelBindingProcessors) { + AnnotatedElement annotatedElement, List channelBindingProcessors) { return channelBindingProcessors.stream() - .map(channelBindingProcessor -> channelBindingProcessor.process(method)) + .map(channelBindingProcessor -> channelBindingProcessor.process(annotatedElement)) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toMap( diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/bindings/processor/TestChannelBindingProcessor.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/bindings/processor/TestChannelBindingProcessor.java index c38c7f412..f9a6bed4d 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/bindings/processor/TestChannelBindingProcessor.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/bindings/processor/TestChannelBindingProcessor.java @@ -13,7 +13,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.Method; +import java.lang.reflect.AnnotatedElement; import java.util.Arrays; import java.util.Optional; @@ -24,8 +24,8 @@ public class TestChannelBindingProcessor implements ChannelBindingProcessor { public static final ChannelBinding BINDING = new EmptyChannelBinding(); @Override - public Optional process(Method method) { - return Arrays.stream(method.getAnnotations()) + public Optional process(AnnotatedElement annotatedElement) { + return Arrays.stream(annotatedElement.getAnnotations()) .filter(annotation -> annotation instanceof TestChannelBindingProcessor.TestChannelBinding) .map(annotation -> (TestChannelBindingProcessor.TestChannelBinding) annotation) .findAny() diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/main/java/io/github/springwolf/examples/cloudstream/configuration/ConsumerClass.java b/springwolf-examples/springwolf-cloud-stream-example/src/main/java/io/github/springwolf/examples/cloudstream/configuration/ConsumerClass.java new file mode 100644 index 000000000..f55cf1a48 --- /dev/null +++ b/springwolf-examples/springwolf-cloud-stream-example/src/main/java/io/github/springwolf/examples/cloudstream/configuration/ConsumerClass.java @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.examples.cloudstream.configuration; + +import io.github.springwolf.examples.cloudstream.dtos.ExamplePayloadDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.function.Consumer; + +@Slf4j +@Component +public class ConsumerClass implements Consumer { + + @Override + public void accept(ExamplePayloadDto payload) { + log.info("Called with payload: {}", payload); + } +} diff --git a/springwolf-examples/springwolf-cloud-stream-example/src/main/resources/application.properties b/springwolf-examples/springwolf-cloud-stream-example/src/main/resources/application.properties index ca03fd0b5..729bf96ad 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/src/main/resources/application.properties +++ b/springwolf-examples/springwolf-cloud-stream-example/src/main/resources/application.properties @@ -10,6 +10,7 @@ spring.kafka.bootstrap-servers=${BOOTSTRAP_SERVER:localhost:29092} spring.cloud.stream.bindings.process-in-0.destination=example-topic spring.cloud.stream.bindings.process-out-0.destination=another-topic spring.cloud.stream.bindings.consumerMethod-in-0.destination=another-topic +spring.cloud.stream.bindings.consumerClass-in-0.destination=consumer-class-topic spring.cloud.stream.bindings.googlePubSubConsumerMethod-in-0.destination=google-pubsub-topic 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 dd0830f25..070780aac 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 @@ -32,6 +32,16 @@ "kafka": { } } }, + "consumer-class-topic": { + "messages": { + "io.github.springwolf.examples.cloudstream.dtos.ExamplePayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.cloudstream.dtos.ExamplePayloadDto" + } + }, + "bindings": { + "kafka": { } + } + }, "example-topic": { "messages": { "io.github.springwolf.examples.cloudstream.dtos.ExamplePayloadDto": { @@ -256,6 +266,21 @@ } ] }, + "consumer-class-topic_publish_ConsumerClass": { + "action": "receive", + "channel": { + "$ref": "#/channels/consumer-class-topic" + }, + "description": "Auto-generated description", + "bindings": { + "kafka": { } + }, + "messages": [ + { + "$ref": "#/channels/consumer-class-topic/messages/io.github.springwolf.examples.cloudstream.dtos.ExamplePayloadDto" + } + ] + }, "example-topic_publish_process": { "action": "receive", "channel": { diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScanner.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScanner.java index 4dc8c3837..55f5f3c49 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScanner.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/channels/CloudStreamFunctionChannelsScanner.java @@ -19,6 +19,7 @@ import io.github.springwolf.core.asyncapi.scanners.beans.BeanMethodsScanner; import io.github.springwolf.core.asyncapi.scanners.bindings.channels.ChannelBindingProcessor; import io.github.springwolf.core.asyncapi.scanners.channels.ChannelMerger; +import io.github.springwolf.core.asyncapi.scanners.classes.spring.ComponentClassScanner; import io.github.springwolf.core.asyncapi.scanners.common.utils.AsyncAnnotationUtil; import io.github.springwolf.core.configuration.docket.AsyncApiDocket; import io.github.springwolf.core.configuration.docket.AsyncApiDocketService; @@ -28,7 +29,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.stream.config.BindingServiceProperties; -import java.lang.reflect.Method; +import java.lang.reflect.AnnotatedElement; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,6 +41,7 @@ public class CloudStreamFunctionChannelsScanner implements ChannelsScanner { private final AsyncApiDocketService asyncApiDocketService; private final BeanMethodsScanner beanMethodsScanner; + private final ComponentClassScanner componentClassScanner; private final ComponentsService componentsService; private final BindingServiceProperties cloudStreamBindingsProperties; private final FunctionalChannelBeanBuilder functionalChannelBeanBuilder; @@ -46,13 +49,18 @@ public class CloudStreamFunctionChannelsScanner implements ChannelsScanner { @Override public Map scan() { - Set beanMethods = beanMethodsScanner.getBeanMethods(); - return ChannelMerger.mergeChannels(beanMethods.stream() - .map(functionalChannelBeanBuilder::fromMethodBean) + Set elements = new HashSet<>(); + elements.addAll(componentClassScanner.scan()); + elements.addAll(beanMethodsScanner.getBeanMethods()); + + List> channels = elements.stream() + .map(functionalChannelBeanBuilder::build) .flatMap(Set::stream) .filter(this::isChannelBean) .map(this::toChannelEntry) - .toList()); + .toList(); + + return ChannelMerger.mergeChannels(channels); } private boolean isChannelBean(FunctionalChannelBeanData beanData) { @@ -88,7 +96,7 @@ private ChannelObject buildChannel(FunctionalChannelBeanData beanData) { .build(); this.componentsService.registerMessage(message); - Map channelBinding = buildChannelBinding(beanData.method()); + Map channelBinding = buildChannelBinding(beanData.annotatedElement()); return ChannelObject.builder() .bindings(channelBinding) .messages(Map.of(message.getName(), MessageReference.toComponentMessage(message))) @@ -101,9 +109,9 @@ private Map buildMessageBinding() { return Map.of(protocolName, new EmptyMessageBinding()); } - private Map buildChannelBinding(Method method) { + private Map buildChannelBinding(AnnotatedElement annotatedElement) { Map channelBindingMap = - AsyncAnnotationUtil.processChannelBindingFromAnnotation(method, channelBindingProcessors); + AsyncAnnotationUtil.processChannelBindingFromAnnotation(annotatedElement, channelBindingProcessors); if (!channelBindingMap.isEmpty()) { return channelBindingMap; } diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilder.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilder.java index 35c2375c1..c8dfd9427 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilder.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilder.java @@ -4,6 +4,7 @@ import io.github.springwolf.core.asyncapi.scanners.common.payload.PayloadClassExtractor; import lombok.RequiredArgsConstructor; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.util.Arrays; @@ -12,6 +13,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import static java.util.stream.Collectors.toList; @@ -20,41 +22,103 @@ 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, payloadType)); + public Set build(AnnotatedElement element) { + Class type = getRawType(element); + + if (Consumer.class.isAssignableFrom(type)) { + Class payloadType = getTypeGenerics(element).get(0); + return Set.of(ofConsumer(element, payloadType)); } - if (Supplier.class.isAssignableFrom(returnType)) { - Class payloadType = getReturnTypeGenerics(methodBean).get(0); - return Set.of(ofSupplier(methodBean, payloadType)); + if (Supplier.class.isAssignableFrom(type)) { + Class payloadType = getTypeGenerics(element).get(0); + return Set.of(ofSupplier(element, payloadType)); } - if (Function.class.isAssignableFrom(returnType)) { - Class inputType = getReturnTypeGenerics(methodBean).get(0); - Class outputType = getReturnTypeGenerics(methodBean).get(1); + if (Function.class.isAssignableFrom(type)) { + Class inputType = getTypeGenerics(element).get(0); + Class outputType = getTypeGenerics(element).get(1); - return Set.of(ofConsumer(methodBean, inputType), ofSupplier(methodBean, outputType)); + return Set.of(ofConsumer(element, inputType), ofSupplier(element, outputType)); } return Collections.emptySet(); } - private static FunctionalChannelBeanData ofConsumer(Method method, Class payloadType) { + private static Class getRawType(AnnotatedElement element) { + if (element instanceof Method m) { + return m.getReturnType(); + } + + if (element instanceof Class c) { + return c; + } + + throw new IllegalArgumentException("Must be a Method or Class"); + } + + private static FunctionalChannelBeanData ofConsumer(AnnotatedElement element, Class payloadType) { + String name = getElementName(element); + String cloudStreamBinding = firstCharToLowerCase(name) + "-in-0"; return new FunctionalChannelBeanData( - method, payloadType, FunctionalChannelBeanData.BeanType.CONSUMER, method.getName() + "-in-0"); + name, element, payloadType, FunctionalChannelBeanData.BeanType.CONSUMER, cloudStreamBinding); } - private static FunctionalChannelBeanData ofSupplier(Method method, Class payloadType) { + private static FunctionalChannelBeanData ofSupplier(AnnotatedElement element, Class payloadType) { + String name = getElementName(element); + String cloudStreamBinding = firstCharToLowerCase(name) + "-out-0"; return new FunctionalChannelBeanData( - method, payloadType, FunctionalChannelBeanData.BeanType.SUPPLIER, method.getName() + "-out-0"); + name, element, payloadType, FunctionalChannelBeanData.BeanType.SUPPLIER, cloudStreamBinding); + } + + private static String firstCharToLowerCase(String name) { + return name.substring(0, 1).toLowerCase() + name.substring(1); + } + + private static String getElementName(AnnotatedElement element) { + if (element instanceof Method m) { + return m.getName(); + } + + if (element instanceof Class c) { + return c.getSimpleName(); + } + + throw new IllegalArgumentException("Must be a Method or Class"); + } + + private List> getTypeGenerics(AnnotatedElement element) { + if (element instanceof Method m) { + ParameterizedType genericReturnType = (ParameterizedType) m.getGenericReturnType(); + return getTypeGenerics(genericReturnType); + } + + if (element instanceof Class c) { + return getTypeGenerics(c); + } + + throw new IllegalArgumentException("Must be a Method or Class"); + } + + private List> getTypeGenerics(Class c) { + Predicate> isConsumerPredicate = Consumer.class::isAssignableFrom; + Predicate> isSupplierPredicate = Supplier.class::isAssignableFrom; + Predicate> isFunctionPredicate = Function.class::isAssignableFrom; + Predicate> hasFunctionalInterfacePredicate = + isConsumerPredicate.or(isSupplierPredicate).or(isFunctionPredicate); + + return Arrays.stream(c.getGenericInterfaces()) + .filter(type -> type instanceof ParameterizedType) + .map(type -> (ParameterizedType) type) + .filter(type -> type.getRawType() instanceof Class) + .filter(type -> hasFunctionalInterfacePredicate.test((Class) type.getRawType())) + .map(this::getTypeGenerics) + .findFirst() + .orElse(Collections.emptyList()); } - private List> getReturnTypeGenerics(Method methodBean) { - ParameterizedType genericReturnType = (ParameterizedType) methodBean.getGenericReturnType(); - return Arrays.stream(genericReturnType.getActualTypeArguments()) + private List> getTypeGenerics(ParameterizedType parameterizedType) { + return Arrays.stream(parameterizedType.getActualTypeArguments()) .map(extractor::typeToClass) .collect(toList()); } diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanData.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanData.java index 4d7a42be4..347a68c13 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanData.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanData.java @@ -1,10 +1,21 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.plugins.cloudstream.asyncapi.scanners.common; -import java.lang.reflect.Method; +import java.lang.reflect.AnnotatedElement; +/** + * @param elementName The simple name of the element (Method or Class). + * @param annotatedElement The element (Method or Class) from which this instance has been processed. + * @param payloadType The payload type of the Channel this bean is bound to. + * @param beanType Consumer or Supplier. + * @param cloudStreamBinding The expected binding string of this bean. + */ public record FunctionalChannelBeanData( - Method method, Class payloadType, BeanType beanType, String cloudStreamBinding) { + String elementName, + AnnotatedElement annotatedElement, + Class payloadType, + BeanType beanType, + String cloudStreamBinding) { public enum BeanType { CONSUMER, diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/operations/CloudStreamFunctionOperationsScanner.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/operations/CloudStreamFunctionOperationsScanner.java index f2d3d9a8c..a7e0b53b6 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/operations/CloudStreamFunctionOperationsScanner.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/operations/CloudStreamFunctionOperationsScanner.java @@ -17,6 +17,7 @@ import io.github.springwolf.core.asyncapi.components.headers.AsyncHeadersNotDocumented; import io.github.springwolf.core.asyncapi.scanners.OperationsScanner; import io.github.springwolf.core.asyncapi.scanners.beans.BeanMethodsScanner; +import io.github.springwolf.core.asyncapi.scanners.classes.spring.ComponentClassScanner; import io.github.springwolf.core.asyncapi.scanners.operations.OperationMerger; import io.github.springwolf.core.configuration.docket.AsyncApiDocket; import io.github.springwolf.core.configuration.docket.AsyncApiDocketService; @@ -26,7 +27,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.stream.config.BindingServiceProperties; -import java.lang.reflect.Method; +import java.lang.reflect.AnnotatedElement; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -37,19 +39,25 @@ public class CloudStreamFunctionOperationsScanner implements OperationsScanner { private final AsyncApiDocketService asyncApiDocketService; private final BeanMethodsScanner beanMethodsScanner; + private final ComponentClassScanner componentClassScanner; private final ComponentsService componentsService; private final BindingServiceProperties cloudStreamBindingsProperties; private final FunctionalChannelBeanBuilder functionalChannelBeanBuilder; @Override public Map scan() { - Set beanMethods = beanMethodsScanner.getBeanMethods(); - return OperationMerger.mergeOperations(beanMethods.stream() - .map(functionalChannelBeanBuilder::fromMethodBean) + Set elements = new HashSet<>(); + elements.addAll(componentClassScanner.scan()); + elements.addAll(beanMethodsScanner.getBeanMethods()); + + List> operations = elements.stream() + .map(functionalChannelBeanBuilder::build) .flatMap(Set::stream) .filter(this::isChannelBean) .map(this::toOperationEntry) - .toList()); + .toList(); + + return OperationMerger.mergeOperations(operations); } private boolean isChannelBean(FunctionalChannelBeanData beanData) { @@ -126,7 +134,6 @@ private String buildOperationId(FunctionalChannelBeanData beanData, String chann String operationName = beanData.beanType() == FunctionalChannelBeanData.BeanType.CONSUMER ? "publish" : "subscribe"; - return String.format( - "%s_%s_%s", channelName, operationName, beanData.method().getName()); + return String.format("%s_%s_%s", channelName, operationName, beanData.elementName()); } } diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/configuration/SpringwolfCloudStreamAutoConfiguration.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/configuration/SpringwolfCloudStreamAutoConfiguration.java index 412e368ff..c3f30478b 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/configuration/SpringwolfCloudStreamAutoConfiguration.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/main/java/io/github/springwolf/plugins/cloudstream/configuration/SpringwolfCloudStreamAutoConfiguration.java @@ -4,6 +4,7 @@ import io.github.springwolf.core.asyncapi.components.ComponentsService; import io.github.springwolf.core.asyncapi.scanners.beans.BeanMethodsScanner; import io.github.springwolf.core.asyncapi.scanners.bindings.channels.ChannelBindingProcessor; +import io.github.springwolf.core.asyncapi.scanners.classes.spring.ComponentClassScanner; import io.github.springwolf.core.asyncapi.scanners.common.payload.PayloadClassExtractor; import io.github.springwolf.core.configuration.docket.AsyncApiDocketService; import io.github.springwolf.core.configuration.properties.SpringwolfConfigConstants; @@ -28,6 +29,7 @@ public class SpringwolfCloudStreamAutoConfiguration { public CloudStreamFunctionChannelsScanner cloudStreamFunctionChannelsScanner( AsyncApiDocketService asyncApiDocketService, BeanMethodsScanner beanMethodsScanner, + ComponentClassScanner componentClassScanner, ComponentsService componentsService, BindingServiceProperties cloudstreamBindingServiceProperties, FunctionalChannelBeanBuilder functionalChannelBeanBuilder, @@ -35,6 +37,7 @@ public CloudStreamFunctionChannelsScanner cloudStreamFunctionChannelsScanner( return new CloudStreamFunctionChannelsScanner( asyncApiDocketService, beanMethodsScanner, + componentClassScanner, componentsService, cloudstreamBindingServiceProperties, functionalChannelBeanBuilder, @@ -45,12 +48,14 @@ public CloudStreamFunctionChannelsScanner cloudStreamFunctionChannelsScanner( public CloudStreamFunctionOperationsScanner cloudStreamFunctionOperationsScanner( AsyncApiDocketService asyncApiDocketService, BeanMethodsScanner beanMethodsScanner, + ComponentClassScanner componentClassScanner, ComponentsService componentsService, BindingServiceProperties cloudstreamBindingServiceProperties, FunctionalChannelBeanBuilder functionalChannelBeanBuilder) { return new CloudStreamFunctionOperationsScanner( asyncApiDocketService, beanMethodsScanner, + componentClassScanner, componentsService, cloudstreamBindingServiceProperties, functionalChannelBeanBuilder); diff --git a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilderTest.java b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilderTest.java index 599a2135d..f81a82c8f 100644 --- a/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilderTest.java +++ b/springwolf-plugins/springwolf-cloud-stream-plugin/src/test/java/io/github/springwolf/plugins/cloudstream/asyncapi/scanners/common/FunctionalChannelBeanBuilderTest.java @@ -11,6 +11,7 @@ import org.springframework.messaging.Message; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -25,123 +26,269 @@ class FunctionalChannelBeanBuilderTest { new FunctionalChannelBeanBuilder(new PayloadClassExtractor(properties)); @Nested - class NoBean { - @Test - void testNotAFunctionalChannelBean() throws NoSuchMethodException { - Method method = getMethod(this.getClass(), "notAFunctionalChannelBean"); + class FromMethod { - Set data = functionalChannelBeanBuilder.fromMethodBean(method); + @Nested + class NotAFunctionalBean { + @Test + void testNotAFunctionalChannelBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "notAFunctionalChannelBean"); - Assertions.assertThat(data).isEmpty(); + Set data = functionalChannelBeanBuilder.build(method); + + Assertions.assertThat(data).isEmpty(); + } + + @Bean + private String notAFunctionalChannelBean() { + return "foo"; + } } - @Bean - private String notAFunctionalChannelBean() { - return "foo"; + @Nested + class ConsumerBean { + @Test + void testConsumerBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "consumerBean"); + + Set data = functionalChannelBeanBuilder.build(method); + + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "consumerBean", method, String.class, CONSUMER, "consumerBean-in-0")); + } + + @Bean + private Consumer consumerBean() { + return System.out::println; + } } - } - @Nested - class ConsumerBean { - @Test - void testConsumerBean() throws NoSuchMethodException { - Method method = getMethod(this.getClass(), "consumerBean"); + @Nested + class SupplierBean { + @Test + void testSupplierBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "supplierBean"); + + Set data = functionalChannelBeanBuilder.build(method); - Set data = functionalChannelBeanBuilder.fromMethodBean(method); + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "supplierBean", method, String.class, SUPPLIER, "supplierBean-out-0")); + } - Assertions.assertThat(data) - .containsExactly( - new FunctionalChannelBeanData(method, String.class, CONSUMER, "consumerBean-in-0")); + @Bean + private Supplier supplierBean() { + return () -> "foo"; + } } - @Bean - private Consumer consumerBean() { - return System.out::println; + @Nested + class FunctionBean { + @Test + void testFunctionBean() throws NoSuchMethodException { + Method method = getMethod(this.getClass(), "functionBean"); + + Set data = functionalChannelBeanBuilder.build(method); + + Assertions.assertThat(data) + .containsExactlyInAnyOrder( + new FunctionalChannelBeanData( + "functionBean", method, String.class, CONSUMER, "functionBean-in-0"), + new FunctionalChannelBeanData( + "functionBean", method, Integer.class, SUPPLIER, "functionBean-out-0")); + } + + @Bean + private Function functionBean() { + return s -> 1; + } } - } - @Nested - class SupplierBean { - @Test - void testSupplierBean() throws NoSuchMethodException { - Method method = getMethod(this.getClass(), "supplierBean"); + @Nested + class ConsumerBeanWithGenericPayload { + + @Test + void testConsumerBeanWithGenericPayload() throws NoSuchMethodException { + String methodName = "consumerBeanWithGenericPayload"; + Method method = getMethod(this.getClass(), methodName); - Set data = functionalChannelBeanBuilder.fromMethodBean(method); + Set data = functionalChannelBeanBuilder.build(method); - Assertions.assertThat(data) - .containsExactly( - new FunctionalChannelBeanData(method, String.class, SUPPLIER, "supplierBean-out-0")); + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "consumerBeanWithGenericPayload", + method, + String.class, + CONSUMER, + methodName + "-in-0")); + } + + @Bean + private Consumer> consumerBeanWithGenericPayload() { + return System.out::println; + } } - @Bean - private Supplier supplierBean() { - return () -> "foo"; + @Nested + class KStreamBean { + + @Test + void testKafkaStreamsConsumerBean() throws NoSuchMethodException { + String methodName = "kafkaStreamsConsumerBean"; + Method method = getMethod(this.getClass(), methodName); + + Set data = functionalChannelBeanBuilder.build(method); + + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "kafkaStreamsConsumerBean", method, String.class, CONSUMER, methodName + "-in-0")); + } + + @Bean + private Consumer> kafkaStreamsConsumerBean() { + return System.out::println; + } } } @Nested - class FunctionBean { - @Test - void testFunctionBean() throws NoSuchMethodException { - Method method = getMethod(this.getClass(), "functionBean"); + class FromClass { + + @Nested + class NotAFunctionalClass { + @Test + void testNotAFunctionalChannelBean() { + Class testClass = getClassObject(this.getClass(), "TestClass"); + + Set data = functionalChannelBeanBuilder.build(testClass); - Set data = functionalChannelBeanBuilder.fromMethodBean(method); + Assertions.assertThat(data).isEmpty(); + } - Assertions.assertThat(data) - .containsExactlyInAnyOrder( - new FunctionalChannelBeanData(method, String.class, CONSUMER, "functionBean-in-0"), - new FunctionalChannelBeanData(method, Integer.class, SUPPLIER, "functionBean-out-0")); + private static class TestClass { + } } - @Bean - private Function functionBean() { - return s -> 1; + @Nested + class ConsumerClass { + @Test + void testConsumerClass() { + Class testClass = getClassObject(this.getClass(), "TestClass"); + + Set data = functionalChannelBeanBuilder.build(testClass); + + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "TestClass", testClass, String.class, CONSUMER, "testClass-in-0")); + } + + private static class TestClass implements Consumer { + @Override + public void accept(String s) { + } + } } - } - @Nested - class ConsumerBeanWithGenericPayload { + @Nested + class SupplierClass { + @Test + void testSupplierClass() { + Class testClass = getClassObject(this.getClass(), "TestClass"); - @Test - void testConsumerBeanWithGenericPayload() throws NoSuchMethodException { - String methodName = "consumerBeanWithGenericPayload"; - Method method = getMethod(this.getClass(), methodName); + Set data = functionalChannelBeanBuilder.build(testClass); - Set data = functionalChannelBeanBuilder.fromMethodBean(method); + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "TestClass", testClass, String.class, SUPPLIER, "testClass-out-0")); + } - Assertions.assertThat(data) - .containsExactly( - new FunctionalChannelBeanData(method, String.class, CONSUMER, methodName + "-in-0")); + private static class TestClass implements Supplier { + + @Override + public String get() { + return "foo"; + } + } } - @Bean - private Consumer> consumerBeanWithGenericPayload() { - return System.out::println; + @Nested + class FunctionClass { + @Test + void testFunctionClass() { + Class testClass = getClassObject(this.getClass(), "TestClass"); + + Set data = functionalChannelBeanBuilder.build(testClass); + + Assertions.assertThat(data) + .containsExactlyInAnyOrder( + new FunctionalChannelBeanData( + "TestClass", testClass, String.class, CONSUMER, "testClass-in-0"), + new FunctionalChannelBeanData( + "TestClass", testClass, Integer.class, SUPPLIER, "testClass-out-0")); + } + + private static class TestClass implements Function { + @Override + public Integer apply(String s) { + return null; + } + } } - } - @Nested - class KStreamBean { + @Nested + class ConsumerClassWithGenericPayload { + + @Test + void testConsumerClassWithGenericPayload() { + Class testClass = getClassObject(this.getClass(), "TestClass"); + + Set data = functionalChannelBeanBuilder.build(testClass); - @Test - void testKafkaStreamsConsumerBean() throws NoSuchMethodException { - String methodName = "kafkaStreamsConsumerBean"; - Method method = getMethod(this.getClass(), methodName); + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "TestClass", testClass, String.class, CONSUMER, "testClass-in-0")); + } - Set data = functionalChannelBeanBuilder.fromMethodBean(method); + static class TestClass implements Consumer> { - Assertions.assertThat(data) - .containsExactly( - new FunctionalChannelBeanData(method, String.class, CONSUMER, methodName + "-in-0")); + @Override + public void accept(Message stringMessage) { + } + } } - @Bean - private Consumer> kafkaStreamsConsumerBean() { - return System.out::println; + @Nested + class KStreamClass { + + @Test + void testKafkaStreamsConsumerClass() { + Class testClass = getClassObject(this.getClass(), "TestClass"); + + Set data = functionalChannelBeanBuilder.build(testClass); + + Assertions.assertThat(data) + .containsExactly(new FunctionalChannelBeanData( + "TestClass", testClass, String.class, CONSUMER, "testClass-in-0")); + } + + static class TestClass implements Consumer> { + @Override + public void accept(KStream voidStringKStream) { + + } + } } } private static Method getMethod(Class clazz, String methodName) throws NoSuchMethodException { return clazz.getDeclaredMethod(methodName); } + + private static Class getClassObject(Class clazz, String className) { + return Arrays.stream(clazz.getDeclaredClasses()) + .filter(c -> c.getSimpleName().equals(className)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find class with name " + className)); + } }