diff --git a/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc b/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc index f371dc11a..03ec08b19 100644 --- a/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc @@ -705,31 +705,177 @@ However, given that `org.springframework.cloud.function.json.JsonMapper` is also [[kotlin-lambda-support]] == Kotlin Lambda support -We also provide support for Kotlin lambdas (since v2.0). -Consider the following: +Spring Cloud Function provides first-class support for Kotlin, allowing developers to leverage idiomatic Kotlin features, including coroutines and Flow, alongside imperative and Reactor-based programming models. -[source, java] +=== Defining Functions in Kotlin + +You can define Suppliers, Functions, and Consumers in Kotlin and register them as Spring beans using several approaches: + +* **Kotlin Lambdas:** Define functions directly as lambda expressions within `@Bean` definitions. This is concise for simple functions. +[source, kotlin] +---- +@Configuration +class MyKotlinConfiguration { + + @Bean + fun kotlinSupplier(): () -> String = { "Hello from Kotlin Lambda" } + + @Bean + fun kotlinFunction(): (String) -> String = { it.uppercase() } + + @Bean + fun kotlinConsumer(): (String) -> Unit = { println("Consumed by Kotlin Lambda: $it") } + } +---- + +* **Kotlin Classes implementing Kotlin Functional Types:** Define a class that directly implements the desired Kotlin functional type (e.g., `(String) -> String`, `suspend () -> Flow`). +[source, kotlin] +---- +@Component +class UppercaseFunction : (String) -> String { +override fun invoke(p1: String): String = p1.uppercase() +} + + // Can also be registered via @Bean +---- + +* **Kotlin Classes implementing `java.util.function` Interfaces:** Define a Kotlin class that implements the standard Java `Supplier`, `Function`, or `Consumer` interfaces. +[source, kotlin] +---- +@Component +class ReverseFunction : Function { +override fun apply(t: String): String = t.reversed() +} +---- + +Regardless of the definition style, beans of these types are registered with the `FunctionCatalog`, benefiting from features like type conversion and composition. + +=== Coroutine Support (`suspend` and `Flow`) + +A key feature is the seamless integration with Kotlin Coroutines. You can use `suspend` functions and `kotlinx.coroutines.flow.Flow` directly in your function signatures. The framework automatically handles the coroutine context and reactive stream conversions. + +* **`suspend` Functions:** Functions marked with `suspend` can perform non-blocking operations using coroutine delays or other suspending calls. +[source, kotlin] ---- @Bean -open fun kotlinSupplier(): () -> String { - return { "Hello from Kotlin" } +fun suspendingFunction(): suspend (String) -> Int = { +delay(100) // Non-blocking delay +it.length } @Bean -open fun kotlinFunction(): (String) -> String { - return { it.toUpperCase() } +fun suspendingSupplier(): suspend () -> String = { + delay(50) + "Data from suspend" } @Bean -open fun kotlinConsumer(): (String) -> Unit { - return { println(it) } +fun suspendingConsumer(): suspend (String) -> Unit = { + delay(20) + println("Suspend consumed: $it") } +---- +* **`Flow` Integration:** Kotlin `Flow` can be used for reactive stream processing, similar to Reactor's `Flux`. +[source, kotlin] ---- -The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of `Supplier`, `Function` and `Consumer`, and thus supported/recognized signatures by the framework. -While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the same rules for signature transformation outlined in "Java 8 function support" section are applied here as well. +@Bean +fun flowFunction(): (Flow) -> Flow = { flow -> +flow.map { it.length } // Process the stream reactively +} + +@Bean +fun flowSupplier(): () -> Flow = { + flow { // kotlinx.coroutines.flow.flow builder + emit("a") + delay(10) + emit("b") + } +} + +// Consumer example taking a Flow +@Bean +fun flowConsumer(): suspend (Flow) -> Unit = { flow -> + flow.collect { item -> // Collect must happen within a coroutine scope + println("Flow consumed: $item") + } +} +---- + +* **Combining `suspend` and `Flow`:** You can combine `suspend` and `Flow` for complex asynchronous and streaming logic. +[source, kotlin] +---- + @Bean + fun suspendingFlowFunction(): suspend (Flow) -> Flow = { incoming -> + flow { + delay(50) // Initial suspend + incoming.collect { + emit(it.uppercase()) // Process and emit + } + } + } + + @Bean + fun suspendingFlowSupplier(): suspend () -> Flow = { + flow { + repeat(3) { + delay(100) + emit(it) + } + } + } +---- + +=== Reactive Types (`Mono`/`Flux`) + +Kotlin functions can seamlessly use Reactor's `Mono` and `Flux` types, just like Java functions. +[source, kotlin] +---- +@Bean +fun reactorFunction(): (Flux) -> Mono = { flux -> + flux.map { it.length }.reduce(0) { acc, i -> acc + i } +} + +@Bean +fun monoSupplier(): () -> Mono = { + Mono.just("Reactive Hello") +} +---- + +=== `Message` Support + +Kotlin functions can also operate directly on `org.springframework.messaging.Message` to access headers, including combinations with `suspend` and `Flow`. +[source, kotlin] +---- +@Bean +fun messageFunction(): (Message) -> Message = { msg -> + MessageBuilder.withPayload(msg.payload.length) + .copyHeaders(msg.headers) + .setHeader("processed", true) + .build() +} + +@Bean +fun suspendMessageFunction(): suspend (Message) -> Message = { msg -> + delay(100) + MessageBuilder.withPayload(msg.payload.reversed()) + .copyHeaders(msg.headers) + .build() +} + +@Bean +fun flowMessageFunction(): (Flow>) -> Flow> = { flow -> + flow.map { msg -> + MessageBuilder.withPayload(msg.payload.hashCode()) + .copyHeaders(msg.headers) + .build() + } +} +---- + +=== Kotlin Sample Project -To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate autoconfiguration and supporting classes. +For a comprehensive set of runnable examples showcasing these Kotlin features, please refer to the `src/test/kotlin/org/springframework/cloud/function/kotlin/arity` directory within the Spring Cloud Function repository. These examples demonstrate a wide range of function signatures with different arities, including regular functions, suspend functions (coroutines), and various reactive types (Flow, Mono, Flux). [[function-component-scan]] == Function Component Scan diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java index 90a783979..effd1c98d 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java @@ -31,7 +31,7 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; -import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionWrapper; import org.springframework.core.KotlinDetector; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -118,7 +118,7 @@ public FunctionRegistration properties(Map properties) { public FunctionRegistration type(Type type) { this.type = type; - if (KotlinDetector.isKotlinPresent() && this.target instanceof KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper) { + if (KotlinDetector.isKotlinPresent() && this.target instanceof KotlinFunctionWrapper) { return this; } Type discoveredFunctionType = type; //FunctionTypeUtils.discoverFunctionTypeFromClass(this.target.getClass()); @@ -174,15 +174,6 @@ public FunctionRegistration names(String... names) { return this.names(Arrays.asList(names)); } - /** - * Transforms (wraps) function identified by the 'target' to its {@code Flux} - * equivalent unless it already is. For example, {@code Function} - * becomes {@code Function, Flux>} - * @param the expected target type of the function (e.g., FluxFunction) - * @return {@code FunctionRegistration} with the appropriately wrapped target. - * - */ - @Override public void setBeanName(String name) { if (CollectionUtils.isEmpty(this.names)) { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java index c5e4ec98f..9cdb6dca5 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java @@ -36,11 +36,12 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.config.FunctionContextUtils; -import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration; +import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionFactory; import org.springframework.cloud.function.context.config.RoutingFunction; import org.springframework.cloud.function.core.FunctionInvocationHelper; import org.springframework.cloud.function.json.JsonMapper; @@ -120,7 +121,8 @@ public T lookup(Class type, String functionDefinition, String... expected functionDefinition = StringUtils.hasText(functionDefinition) ? functionDefinition : this.applicationContext.getEnvironment().getProperty(FunctionProperties.FUNCTION_DEFINITION, ""); - if (!this.applicationContext.containsBean(functionDefinition) || !KotlinUtils.isKotlinType(this.applicationContext.getBean(functionDefinition))) { + + if (!this.applicationContext.containsBean(functionDefinition) || !isKotlinType(functionDefinition)) { functionDefinition = this.normalizeFunctionDefinition(functionDefinition); } if (!isFunctionDefinitionEligible(functionDefinition)) { @@ -160,12 +162,9 @@ public T lookup(Class type, String functionDefinition, String... expected else if (functionCandidate instanceof BiFunction || functionCandidate instanceof BiConsumer) { functionRegistration = this.registerMessagingBiFunction(functionCandidate, functionName); } - else if (KotlinUtils.isKotlinType(functionCandidate)) { - KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper wrapper = - new KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper(functionCandidate); - wrapper.setName(functionName); - wrapper.setBeanFactory(this.applicationContext.getBeanFactory()); - functionRegistration = wrapper.getFunctionRegistration(); + else if (isKotlinType(functionName, functionCandidate)) { + KotlinLambdaToFunctionFactory kotlinFactory = new KotlinLambdaToFunctionFactory(functionCandidate, this.applicationContext.getBeanFactory()); + functionRegistration = kotlinFactory.getFunctionRegistration(functionName); } else if (this.isFunctionPojo(functionCandidate, functionName)) { Method functionalMethod = FunctionTypeUtils.discoverFunctionalMethod(functionCandidate.getClass()); @@ -203,6 +202,17 @@ else if (this.isSpecialFunctionRegistration(functionNames, functionName)) { return (T) function; } + private boolean isKotlinType(String functionDefinition) { + Object fonctionBean = this.applicationContext.getBean(functionDefinition); + return isKotlinType(functionDefinition, fonctionBean); + } + + private boolean isKotlinType(String functionDefinition, Object fonctionBean) { + ConfigurableListableBeanFactory beanFactory = this.applicationContext.getBeanFactory(); + Type functionType = FunctionContextUtils.findType(functionDefinition, beanFactory); + return KotlinUtils.isKotlinType(fonctionBean, functionType); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) private FunctionRegistration registerMessagingBiFunction(Object userFunction, String functionName) { Type biFunctionType = FunctionContextUtils.findType(this.applicationContext.getBeanFactory(), functionName); diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java index 3fc05f078..6cbce9075 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java @@ -48,6 +48,7 @@ import com.fasterxml.jackson.databind.JsonNode; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; @@ -234,6 +235,10 @@ else if (Function0.class.isAssignableFrom(functionalClass)) { ResolvableType kotlinType = ResolvableType.forClass(functionalClass).as(Function0.class); return GenericTypeResolver.resolveType(kotlinType.getType(), functionalClass); } + else if (Function2.class.isAssignableFrom(functionalClass)) { + ResolvableType kotlinType = ResolvableType.forClass(functionalClass).as(Function2.class); + return GenericTypeResolver.resolveType(kotlinType.getType(), functionalClass); + } } Type typeToReturn = null; if (Function.class.isAssignableFrom(functionalClass)) { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java deleted file mode 100644 index e40aefd8f..000000000 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.context.config; - -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import kotlin.Unit; -import kotlin.jvm.functions.Function0; -import kotlin.jvm.functions.Function1; -import kotlin.jvm.functions.Function2; -import kotlin.jvm.functions.Function3; -import kotlin.jvm.functions.Function4; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.ResolvableType; -import org.springframework.util.ObjectUtils; - -/** - * Configuration class which defines the required infrastructure to bootstrap Kotlin - * lambdas as invocable functions within the context of the framework. - * - * @author Oleg Zhurakousky - * @author Adrien Poupard - * @author Dmitriy Tsypov - * @since 2.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(name = "kotlin.jvm.functions.Function0") -public class KotlinLambdaToFunctionAutoConfiguration { - - protected final Log logger = LogFactory.getLog(getClass()); - - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static final class KotlinFunctionWrapper implements Function, Supplier, Consumer, - Function0, Function1, Function2, - Function3, Function4 { - private final Object kotlinLambdaTarget; - - private String name; - - private ConfigurableListableBeanFactory beanFactory; - - public KotlinFunctionWrapper(Object kotlinLambdaTarget) { - this.kotlinLambdaTarget = kotlinLambdaTarget; - } - - @Override - public Object apply(Object input) { - if (ObjectUtils.isEmpty(input)) { - return this.invoke(); - } - else { - return this.invoke(input); - } - } - - @Override - public Object invoke(Object arg0, Object arg1, Object arg2, Object arg3) { - return ((Function4) this.kotlinLambdaTarget).invoke(arg0, arg1, arg2, arg3); - } - - @Override - public Object invoke(Object arg0, Object arg1, Object arg2) { - return ((Function3) this.kotlinLambdaTarget).invoke(arg0, arg1, arg2); - } - - @Override - public Object invoke(Object arg0, Object arg1) { - return ((Function2) this.kotlinLambdaTarget).invoke(arg0, arg1); - } - - @Override - public Object invoke(Object arg0) { - if (CoroutinesUtils.isValidSuspendingFunction(kotlinLambdaTarget, arg0)) { - return CoroutinesUtils.invokeSuspendingFunction(kotlinLambdaTarget, arg0); - } - if (this.kotlinLambdaTarget instanceof Function1) { - return ((Function1) this.kotlinLambdaTarget).invoke(arg0); - } - else if (this.kotlinLambdaTarget instanceof Function) { - return ((Function) this.kotlinLambdaTarget).apply(arg0); - } - ((Consumer) this.kotlinLambdaTarget).accept(arg0); - return null; - } - - @Override - public Object invoke() { - if (CoroutinesUtils.isValidSuspendingSupplier(kotlinLambdaTarget)) { - return CoroutinesUtils.invokeSuspendingSupplier(kotlinLambdaTarget); - } - if (this.kotlinLambdaTarget instanceof Function0) { - return ((Function0) this.kotlinLambdaTarget).invoke(); - } - return ((Supplier) this.kotlinLambdaTarget).get(); - } - - @Override - public void accept(Object input) { - if (CoroutinesUtils.isValidSuspendingFunction(kotlinLambdaTarget, input)) { - CoroutinesUtils.invokeSuspendingConsumer(kotlinLambdaTarget, input); - return; - } - this.apply(input); - } - - @Override - public Object get() { - return this.apply(null); - } - - public FunctionRegistration getFunctionRegistration() { - String name = this.name.endsWith(FunctionRegistration.REGISTRATION_NAME_SUFFIX) - ? this.name.replace(FunctionRegistration.REGISTRATION_NAME_SUFFIX, "") - : this.name; - Type functionType = FunctionContextUtils.findType(name, this.beanFactory); - FunctionRegistration registration = new FunctionRegistration<>(this, name); - Type[] types = ((ParameterizedType) functionType).getActualTypeArguments(); - - if (isValidKotlinSupplier(functionType)) { - functionType = ResolvableType.forClassWithGenerics(Supplier.class, ResolvableType.forType(types[0])) - .getType(); - } - else if (isValidKotlinConsumer(functionType, types)) { - functionType = ResolvableType.forClassWithGenerics(Consumer.class, ResolvableType.forType(types[0])) - .getType(); - } - else if (isValidKotlinFunction(functionType, types)) { - functionType = ResolvableType.forClassWithGenerics(Function.class, ResolvableType.forType(types[0]), - ResolvableType.forType(types[1])).getType(); - } - else if (isValidKotlinSuspendSupplier(functionType, types)) { - Type continuationReturnType = CoroutinesUtils.getSuspendingFunctionReturnType(types[0]); - functionType = ResolvableType.forClassWithGenerics( - Supplier.class, - ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(continuationReturnType)) - ).getType(); - } - else if (isValidKotlinSuspendFunction(functionType, types)) { - Type continuationArgType = CoroutinesUtils.getSuspendingFunctionArgType(types[0]); - Type continuationReturnType = CoroutinesUtils.getSuspendingFunctionReturnType(types[1]); - functionType = ResolvableType.forClassWithGenerics( - Function.class, - ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(continuationArgType)), - ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(continuationReturnType)) - ).getType(); - } - else if (isValidKotlinSuspendConsumer(functionType, types)) { - Type continuationArgType = CoroutinesUtils.getSuspendingFunctionArgType(types[0]); - functionType = ResolvableType.forClassWithGenerics( - Consumer.class, - ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(continuationArgType)) - ).getType(); - } - else if (!FunctionTypeUtils.isFunction(functionType) - && !FunctionTypeUtils.isConsumer(functionType) - && !FunctionTypeUtils.isSupplier(functionType)) { - throw new UnsupportedOperationException("Multi argument Kotlin functions are not currently supported"); - } - registration = registration.type(functionType); - return registration; - } - - private boolean isValidKotlinSupplier(Type functionType) { - return isTypeRepresentedByClass(functionType, Function0.class); - } - - private boolean isValidKotlinConsumer(Type functionType, Type[] type) { - return isTypeRepresentedByClass(functionType, Function1.class) && - type.length == 2 && - !CoroutinesUtils.isContinuationType(type[0]) && - isTypeRepresentedByClass(type[1], Unit.class); - } - - private boolean isValidKotlinFunction(Type functionType, Type[] type) { - return isTypeRepresentedByClass(functionType, Function1.class) && - type.length == 2 && - !CoroutinesUtils.isContinuationType(type[0]) && - !isTypeRepresentedByClass(type[1], Unit.class); - } - - private boolean isValidKotlinSuspendSupplier(Type functionType, Type[] type) { - return isTypeRepresentedByClass(functionType, Function1.class) && - type.length == 2 && - CoroutinesUtils.isContinuationFlowType(type[0]); - } - - private boolean isValidKotlinSuspendConsumer(Type functionType, Type[] type) { - return isTypeRepresentedByClass(functionType, Function2.class) && - type.length == 3 && - CoroutinesUtils.isFlowType(type[0]) && - CoroutinesUtils.isContinuationUnitType(type[1]); - } - - private boolean isValidKotlinSuspendFunction(Type functionType, Type[] type) { - return isTypeRepresentedByClass(functionType, Function2.class) && - type.length == 3 && - CoroutinesUtils.isContinuationFlowType(type[1]); - } - - private boolean isTypeRepresentedByClass(Type type, Class clazz) { - return type.getTypeName().contains(clazz.getName()); - } - - public Class getObjectType() { - return FunctionRegistration.class; - } - - - public void setName(String name) { - this.name = name; - } - - - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } - } -} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionFactory.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionFactory.java new file mode 100644 index 000000000..35b21fc6e --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionFactory.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.config; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.cloud.function.context.FunctionRegistration; +import org.springframework.cloud.function.context.wrapper.KotlinConsumerFlowWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinConsumerPlainWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinConsumerSuspendFlowWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinConsumerSuspendPlainWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionFlowToFlowWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionFlowToPlainWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionPlainToFlowWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionPlainToPlainWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionSuspendFlowToFlowWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionSuspendFlowToPlainWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionSuspendPlainToFlowWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionSuspendPlainToPlainWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinFunctionWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinSupplierFlowWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinSupplierPlainWrapper; +import org.springframework.cloud.function.context.wrapper.KotlinSupplierSuspendWrapper; + +/** + * Factory for creating Kotlin function wrappers. + * @author Adrien Poupard + */ +public final class KotlinLambdaToFunctionFactory { + + private final Object kotlinLambdaTarget; + private final ConfigurableListableBeanFactory beanFactory; + + public KotlinLambdaToFunctionFactory(Object kotlinLambdaTarget, ConfigurableListableBeanFactory beanFactory) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.beanFactory = beanFactory; + } + + public FunctionRegistration getFunctionRegistration(String functionName) { + String name = functionName.endsWith(FunctionRegistration.REGISTRATION_NAME_SUFFIX) + ? functionName.replace(FunctionRegistration.REGISTRATION_NAME_SUFFIX, "") + : functionName; + Type functionType = FunctionContextUtils.findType(name, beanFactory); + Type[] types = ((ParameterizedType) functionType).getActualTypeArguments(); + KotlinFunctionWrapper wrapper = null; + if (KotlinConsumerFlowWrapper.isValid(functionType, types)) { + wrapper = KotlinConsumerFlowWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinConsumerPlainWrapper.isValid(functionType, types)) { + wrapper = KotlinConsumerPlainWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinConsumerSuspendFlowWrapper.isValid(functionType, types)) { + wrapper = KotlinConsumerSuspendFlowWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinConsumerSuspendPlainWrapper.isValid(functionType, types)) { + wrapper = KotlinConsumerSuspendPlainWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionFlowToFlowWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionFlowToFlowWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionSuspendFlowToFlowWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionSuspendFlowToFlowWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionFlowToPlainWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionFlowToPlainWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionSuspendFlowToPlainWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionSuspendFlowToPlainWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionPlainToFlowWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionPlainToFlowWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionSuspendPlainToFlowWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionSuspendPlainToFlowWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinSupplierFlowWrapper.isValid(functionType, types)) { + wrapper = KotlinSupplierFlowWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinSupplierPlainWrapper.isValid(functionType, types)) { + wrapper = KotlinSupplierPlainWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinSupplierSuspendWrapper.isValid(functionType, types)) { + wrapper = KotlinSupplierSuspendWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionPlainToPlainWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionPlainToPlainWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + else if (KotlinFunctionSuspendPlainToPlainWrapper.isValid(functionType, types)) { + wrapper = KotlinFunctionSuspendPlainToPlainWrapper.asRegistrationFunction(functionName, kotlinLambdaTarget, types); + } + if (wrapper == null) { + throw new IllegalStateException("Unable to create function wrapper for " + functionName); + } + + FunctionRegistration registration = new FunctionRegistration<>(wrapper, wrapper.getName()); + registration.type(wrapper.getResolvableType().getType()); + return registration; + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerFlowWrapper.java new file mode 100644 index 000000000..aed3799d8 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerFlowWrapper.java @@ -0,0 +1,98 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + + +/** + * The KotlinConsumerFlowWrapper class serves as a wrapper for a Kotlin consumer function + * that consumes a Flow of objects and provides integration with Reactor's Flux API, + * bridging the gap between Kotlin's Flow and Java's reactive streams. + * + * @author Adrien Poupard + */ +public final class KotlinConsumerFlowWrapper + implements KotlinFunctionWrapper, Consumer>, Function1, Unit> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinConsumer(functionType, types) && TypeUtils.isFlowType(types[0]); + } + + public static KotlinConsumerFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, + Type[] propsTypes) { + ResolvableType props = ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(propsTypes[0])); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Consumer.class, props); + return new KotlinConsumerFlowWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinConsumerFlowWrapper(Object kotlinLambdaTarget, ResolvableType type, + String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + this.name = functionName; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void accept(Flux props) { + Flow flow = TypeUtils.convertToFlow(props); + invoke(flow); + } + + @Override + public Unit invoke(Flow props) { + if (kotlinLambdaTarget instanceof Function1) { + Function1, Unit> function = (Function1, Unit>) kotlinLambdaTarget; + return function.invoke(props); + } + else if (kotlinLambdaTarget instanceof Consumer) { + Consumer> target = (Consumer>) kotlinLambdaTarget; + target.accept(props); + return Unit.INSTANCE; + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerPlainWrapper.java new file mode 100644 index 000000000..79be8a243 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerPlainWrapper.java @@ -0,0 +1,95 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinConsumerPlainWrapper class serves as a bridge for Kotlin consumer functions + * that process regular objects, enabling their integration within the Spring Cloud + * Function framework. + * + * @author Adrien Poupard + */ +public final class KotlinConsumerPlainWrapper implements KotlinFunctionWrapper, Consumer, Function1 { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinConsumer(functionType, types) && !TypeUtils.isFlowType(types[0]); + } + + public static KotlinConsumerPlainWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, + Type[] propsTypes) { + ResolvableType functionType = ResolvableType.forClassWithGenerics( + Consumer.class, + ResolvableType.forType(propsTypes[0]) + ); + return new KotlinConsumerPlainWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinConsumerPlainWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.name = functionName; + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void accept(Object input) { + invoke(input); + } + + @Override + public Unit invoke(Object input) { + if (this.kotlinLambdaTarget instanceof Function1) { + // Call the function but don't try to cast the result to Unit + // This handles cases where the function returns something other than Unit (e.g., MonoRunnable) + ((Function1) this.kotlinLambdaTarget).invoke(input); + return Unit.INSTANCE; + } + else if (this.kotlinLambdaTarget instanceof Consumer) { + ((Consumer) this.kotlinLambdaTarget).accept(input); + return Unit.INSTANCE; + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendFlowWrapper.java new file mode 100644 index 000000000..2060ed07e --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendFlowWrapper.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.CoroutinesUtils; +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinConsumerSuspendFlowWrapper class serves as a bridge for Kotlin suspending + * consumer functions that process Flow objects, enabling their integration within the + * Spring Cloud Function framework's reactive programming model. + * + * @author Adrien Poupard + */ +public final class KotlinConsumerSuspendFlowWrapper implements KotlinFunctionWrapper, Consumer>, Function1, Unit> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSuspendConsumer(functionType, types) && TypeUtils.isFlowType(types[0]); + } + + public static KotlinConsumerSuspendFlowWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType continuationArgType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Consumer.class, + ResolvableType.forClassWithGenerics(Flux.class, continuationArgType)); + return new KotlinConsumerSuspendFlowWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private String name; + + private final ResolvableType type; + + public KotlinConsumerSuspendFlowWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.name = functionName; + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void accept(Flux input) { + invoke(input); + } + + @Override + public Unit invoke(Flux input) { + CoroutinesUtils.invokeSuspendingConsumerFlow(kotlinLambdaTarget, input); + return Unit.INSTANCE; + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendPlainWrapper.java new file mode 100644 index 000000000..63bd0e761 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendPlainWrapper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; + +import org.springframework.cloud.function.context.config.CoroutinesUtils; +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinConsumerSuspendPlainWrapper class serves as a bridge for Kotlin suspending + * consumer functions that process regular objects, enabling their integration within the + * Spring Cloud Function framework. + * + * @author Adrien Poupard + */ +public final class KotlinConsumerSuspendPlainWrapper implements KotlinFunctionWrapper, Consumer, Function1 { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSuspendConsumer(functionType, types) && !TypeUtils.isFlowType(types[0]); + } + + public static KotlinConsumerSuspendPlainWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType continuationArgType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Consumer.class, continuationArgType); + return new KotlinConsumerSuspendPlainWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private String name; + + private final ResolvableType type; + + public KotlinConsumerSuspendPlainWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.name = functionName; + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void accept(Object input) { + invoke(input); + } + + @Override + public Unit invoke(Object input) { + CoroutinesUtils.invokeSuspendingConsumer(kotlinLambdaTarget, input); + return Unit.INSTANCE; + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToFlowWrapper.java new file mode 100644 index 000000000..4ca47d9a4 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToFlowWrapper.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionFlowToFlowWrapper class serves as a bridge for Kotlin functions that + * process Flow objects, converting both input and output between Kotlin's Flow and Java's + * Flux for seamless integration with Spring Cloud Function's reactive programming model. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionFlowToFlowWrapper + implements KotlinFunctionWrapper, Function, Flux>, Function1, Flux> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinFunction(functionType, types) && types.length == 2 + && TypeUtils.isFlowType(types[0]) && TypeUtils.isFlowType(types[1]); + } + + public static KotlinFunctionFlowToFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, + Type[] propsTypes) { + ResolvableType props = ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(propsTypes[0])); + ResolvableType result = ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(propsTypes[1])); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, props, result); + return new KotlinFunctionFlowToFlowWrapper( + kotlinLambdaTarget, + functionType, + functionName + ); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinFunctionFlowToFlowWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.name = functionName; + this.type = type; + } + + @Override + public Flux apply(Flux input) { + return this.invoke(input); + } + + @Override + public Flux invoke(Flux arg0) { + Flow flow = TypeUtils.convertToFlow(arg0); + if (kotlinLambdaTarget instanceof Function1) { + Function1, Flow> target = (Function1, Flow>) kotlinLambdaTarget; + Flow result = target.invoke(flow); + return TypeUtils.convertToFlux(result); + } + else if (kotlinLambdaTarget instanceof Function) { + Function, Flow> target = (Function, Flow>) kotlinLambdaTarget; + Flow result = target.apply(flow); + return TypeUtils.convertToFlux(result); + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToPlainWrapper.java new file mode 100644 index 000000000..84841feff --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToPlainWrapper.java @@ -0,0 +1,96 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionFlowToPlainWrapper class serves as a bridge for Kotlin functions that + * take Flow objects as input and produce regular objects as output, enabling their + * integration within the Spring Cloud Function framework's reactive programming model. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionFlowToPlainWrapper + implements KotlinFunctionWrapper, Function, Object>, Function1, Object> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinFunction(functionType, types) && types.length == 2 + && TypeUtils.isFlowType(types[0]) && !TypeUtils.isFlowType(types[1]); + } + + public static KotlinFunctionFlowToPlainWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType props = ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(propsTypes[0])); + ResolvableType result = ResolvableType.forType(propsTypes[1]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, props, result); + return new KotlinFunctionFlowToPlainWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinFunctionFlowToPlainWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.name = functionName; + this.type = type; + } + + @Override + public Object invoke(Flux arg0) { + Flow flow = TypeUtils.convertToFlow(arg0); + if (kotlinLambdaTarget instanceof Function) { + Function, Object> target = (Function, Object>) kotlinLambdaTarget; + return target.apply(flow); + } + else if (kotlinLambdaTarget instanceof Function1) { + Function1, Object> target = (Function1, Object>) kotlinLambdaTarget; + return target.invoke(flow); + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Object apply(Flux input) { + return this.invoke(input); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToFlowWrapper.java new file mode 100644 index 000000000..303601e16 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToFlowWrapper.java @@ -0,0 +1,98 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionPlainToFlowWrapper class serves as a bridge for Kotlin functions that + * take regular objects as input and produce Flow objects as output, converting them to + * Flux objects for seamless integration with Spring Cloud Function's reactive programming + * model. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionPlainToFlowWrapper + implements KotlinFunctionWrapper, Function>, Function1> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinFunction(functionType, types) && types.length == 2 + && !TypeUtils.isFlowType(types[0]) && TypeUtils.isFlowType(types[1]); + } + + public static KotlinFunctionPlainToFlowWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType props = ResolvableType.forType(propsTypes[0]); + ResolvableType result = ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(propsTypes[1])); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, props, result); + return new KotlinFunctionPlainToFlowWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinFunctionPlainToFlowWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.name = functionName; + this.type = type; + } + + @Override + public Flux invoke(Object arg0) { + if (kotlinLambdaTarget instanceof Function) { + Function> target = (Function>) kotlinLambdaTarget; + Flow result = target.apply(arg0); + return TypeUtils.convertToFlux(result); + } + else if (kotlinLambdaTarget instanceof Function1) { + Function1> target = (Function1>) kotlinLambdaTarget; + Flow result = target.invoke(arg0); + return TypeUtils.convertToFlux(result); + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Flux apply(Object input) { + return this.invoke(input); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToPlainWrapper.java new file mode 100644 index 000000000..29f22460c --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToPlainWrapper.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionObjectToObjectWrapper class serves as a wrapper for Kotlin functions, + * enabling seamless integration between Kotlin's functional types and Java's Function + * interface within the Spring Cloud Function framework. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionPlainToPlainWrapper + implements KotlinFunctionWrapper, Function, Function1 { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinFunction(functionType, types); + } + + public static KotlinFunctionPlainToPlainWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType type = ResolvableType.forClassWithGenerics(Function.class, ResolvableType.forType(propsTypes[0]), + ResolvableType.forType(propsTypes[1])); + return new KotlinFunctionPlainToPlainWrapper(kotlinLambdaTarget, type, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinFunctionPlainToPlainWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.name = functionName; + this.type = type; + } + + @Override + public Object invoke(Object arg0) { + if (this.kotlinLambdaTarget instanceof Function1) { + return ((Function1) this.kotlinLambdaTarget).invoke(arg0); + } + else if (this.kotlinLambdaTarget instanceof Function) { + return ((Function) this.kotlinLambdaTarget).apply(arg0); + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Object apply(Object input) { + return this.invoke(input); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToFlowWrapper.java new file mode 100644 index 000000000..f95838b2c --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToFlowWrapper.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.CoroutinesUtils; +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionSuspendFlowToFlowWrapper class serves as a wrapper for a Kotlin + * suspending function that consumes a Flow and produces a Flow. It adapts the Kotlin + * suspending function into a Java {@link Function} and provides support for integration + * with frameworks requiring reactive streams such as Reactor. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionSuspendFlowToFlowWrapper + implements KotlinFunctionWrapper, Function, Flux>, Function1, Flux> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 + && TypeUtils.isFlowType(types[0]) && TypeUtils.isContinuationFlowType(types[1]); + } + + public static KotlinFunctionSuspendFlowToFlowWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType argType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType returnType = TypeUtils.getSuspendingFunctionReturnType(propsTypes[1]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, + ResolvableType.forClassWithGenerics(Flux.class, argType), + ResolvableType.forClassWithGenerics(Flux.class, returnType)); + return new KotlinFunctionSuspendFlowToFlowWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinFunctionSuspendFlowToFlowWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.name = functionName; + this.type = type; + } + + @Override + public Flux invoke(Flux arg0) { + Flow flow = TypeUtils.convertToFlow(arg0); + return CoroutinesUtils.invokeSuspendingFlowFunction(kotlinLambdaTarget, flow); + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Flux apply(Flux input) { + return this.invoke(input); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToPlainWrapper.java new file mode 100644 index 000000000..2128f8b08 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToPlainWrapper.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.CoroutinesUtils; +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionSuspendFlowToPlainWrapper class serves as a wrapper that adapts a + * Kotlin suspend function with a Flow input to a synchronous function, making it + * compatible with Java-based functional constructs such as {@link Function}. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionSuspendFlowToPlainWrapper + implements KotlinFunctionWrapper, Function, Object>, Function1, Object> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 + && TypeUtils.isFlowType(types[0]) && TypeUtils.isContinuationType(types[1]) + && !TypeUtils.isContinuationFlowType(types[1]); + } + + public static KotlinFunctionSuspendFlowToPlainWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType argType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType result = TypeUtils.getSuspendingFunctionReturnType(propsTypes[1]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, + ResolvableType.forClassWithGenerics(Flux.class, argType), result); + return new KotlinFunctionSuspendFlowToPlainWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinFunctionSuspendFlowToPlainWrapper(Object kotlinLambdaTarget, ResolvableType type, + String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.name = functionName; + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public Object invoke(Flux arg0) { + Flow flow = TypeUtils.convertToFlow(arg0); + return CoroutinesUtils.invokeSuspendingFlowFunction(kotlinLambdaTarget, flow); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Object apply(Flux input) { + return this.invoke(input); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToFlowWrapper.java new file mode 100644 index 000000000..9245cd3af --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToFlowWrapper.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.CoroutinesUtils; +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionSuspendPlainToFlowWrapper class serves as a bridge for Kotlin + * suspending functions that take regular objects as input and produce Flow objects as + * output, enabling their integration within the Spring Cloud Function framework's + * reactive programming model. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionSuspendPlainToFlowWrapper + implements KotlinFunctionWrapper, Function>, Function1> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 + && !TypeUtils.isFlowType(types[0]) && TypeUtils.isContinuationFlowType(types[1]); + } + + public static KotlinFunctionSuspendPlainToFlowWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType argType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType returnType = TypeUtils.getSuspendingFunctionReturnType(propsTypes[1]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, argType, + ResolvableType.forClassWithGenerics(Flux.class, returnType)); + return new KotlinFunctionSuspendPlainToFlowWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinFunctionSuspendPlainToFlowWrapper(Object kotlinLambdaTarget, ResolvableType type, + String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.name = functionName; + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public Flux invoke(Object arg0) { + return CoroutinesUtils.invokeSuspendingSingleFunction(kotlinLambdaTarget, arg0); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Flux apply(Object input) { + return this.invoke(input); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToPlainWrapper.java new file mode 100644 index 000000000..274adf1ee --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToPlainWrapper.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.CoroutinesUtils; +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +/** + * The KotlinFunctionSuspendPlainToPlainWrapper class serves as a bridge for Kotlin + * suspending functions that transform input objects to output objects, enabling their + * integration within the Spring Cloud Function framework's reactive programming model. + * + * @author Adrien Poupard + */ +public final class KotlinFunctionSuspendPlainToPlainWrapper + implements KotlinFunctionWrapper, Function, Function1 { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSuspendFunction(functionType, types); + } + + public static KotlinFunctionSuspendPlainToPlainWrapper asRegistrationFunction(String functionName, + Object kotlinLambdaTarget, Type[] propsTypes) { + ResolvableType argType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType returnType = TypeUtils.getSuspendingFunctionReturnType(propsTypes[1]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, argType, + ResolvableType.forClassWithGenerics(Flux.class, returnType)); + return new KotlinFunctionSuspendPlainToPlainWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private String name; + + private final ResolvableType type; + + public KotlinFunctionSuspendPlainToPlainWrapper(Object kotlinLambdaTarget, ResolvableType type, + String functionName) { + this.name = functionName; + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public Object invoke(Object arg0) { + return CoroutinesUtils.invokeSuspendingSingleFunction(kotlinLambdaTarget, arg0); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Object apply(Object input) { + return this.invoke(input); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionWrapper.java new file mode 100644 index 000000000..51493de8a --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionWrapper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import org.springframework.core.ResolvableType; + +/** + * @author Adrien Poupard + */ +public interface KotlinFunctionWrapper { + + ResolvableType getResolvableType(); + + String getName(); + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierFlowWrapper.java new file mode 100644 index 000000000..2ccc2be30 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierFlowWrapper.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Supplier; + +import kotlin.jvm.functions.Function0; +import kotlinx.coroutines.flow.Flow; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; + +import static org.springframework.cloud.function.context.config.TypeUtils.convertToFlux; + +/** + * The KotlinSupplierFlowWrapper class serves as a wrapper to integrate Kotlin's Function0 + * with Java's Supplier interface and transform Kotlin Flow objects to Reactor Flux + * objects, bridging functional paradigms between Kotlin and Java within the Spring Cloud + * Function framework. + * + * @author Adrien Poupard + */ +public final class KotlinSupplierFlowWrapper + implements KotlinFunctionWrapper, Supplier>, Function0> { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSupplier(functionType) && types.length == 1 && TypeUtils.isFlowType(types[0]); + } + + public static KotlinSupplierFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, + Type[] propsTypes) { + + ResolvableType props = ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(propsTypes[0])); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Supplier.class, props); + + return new KotlinSupplierFlowWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinSupplierFlowWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + this.name = functionName; + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Flux get() { + Flow result = invoke(); + return convertToFlux(result); + } + + @Override + public Flow invoke() { + if (kotlinLambdaTarget instanceof Function0) { + Function0> target = (Function0>) kotlinLambdaTarget; + Flow result = target.invoke(); + return result; + } + else if (kotlinLambdaTarget instanceof Supplier) { + Supplier> target = (Supplier>) kotlinLambdaTarget; + Flow result = target.get(); + return result; + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierPlainWrapper.java new file mode 100644 index 000000000..985430770 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierPlainWrapper.java @@ -0,0 +1,96 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Supplier; + +import kotlin.jvm.functions.Function0; + +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.core.ResolvableType; +import org.springframework.util.ObjectUtils; + +/** + * The KotlinSupplierPlainWrapper class serves as a bridge for Kotlin supplier functions + * that return regular objects, enabling their integration within the Spring Cloud + * Function framework. + * + * @author Adrien Poupard + * + */ +public final class KotlinSupplierPlainWrapper implements KotlinFunctionWrapper, Supplier, Function0 { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSupplier(functionType); + } + + public static KotlinSupplierPlainWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, + Type[] propsTypes) { + ResolvableType functionType = ResolvableType.forClassWithGenerics(Supplier.class, + ResolvableType.forType(propsTypes[0])); + return new KotlinSupplierPlainWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinSupplierPlainWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.name = functionName; + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + } + + public Object apply(Object input) { + if (ObjectUtils.isEmpty(input)) { + return this.get(); + } + return null; + } + + @Override + public Object get() { + return invoke(); + } + + @Override + public Object invoke() { + if (this.kotlinLambdaTarget instanceof Function0) { + return ((Function0) this.kotlinLambdaTarget).invoke(); + } + else if (this.kotlinLambdaTarget instanceof Supplier) { + return ((Supplier) this.kotlinLambdaTarget).get(); + } + else { + throw new IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.getClass()); + } + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierSuspendWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierSuspendWrapper.java new file mode 100644 index 000000000..7a42282fb --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierSuspendWrapper.java @@ -0,0 +1,91 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.wrapper; + +import java.lang.reflect.Type; +import java.util.function.Supplier; + +import kotlin.jvm.functions.Function0; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.context.config.CoroutinesUtils; +import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.core.ResolvableType; +import org.springframework.util.ObjectUtils; + +/** + * The KotlinSupplierSuspendWrapper class serves as a bridge between Kotlin suspending + * supplier functions and Java's Supplier interface, enabling seamless integration of + * Kotlin coroutines within the Spring Cloud Function framework. + * + * @author Adrien Poupard + */ +public final class KotlinSupplierSuspendWrapper implements KotlinFunctionWrapper, Supplier, Function0 { + + public static Boolean isValid(Type functionType, Type[] types) { + return FunctionUtils.isValidKotlinSuspendSupplier(functionType, types); + } + + public static KotlinSupplierSuspendWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, + Type[] propsTypes) { + ResolvableType returnType = TypeUtils.getSuspendingFunctionReturnType(propsTypes[0]); + ResolvableType functionType = ResolvableType.forClassWithGenerics(Supplier.class, + ResolvableType.forClassWithGenerics(Flux.class, returnType)); + return new KotlinSupplierSuspendWrapper(kotlinLambdaTarget, functionType, functionName); + } + + private final Object kotlinLambdaTarget; + + private final String name; + + private final ResolvableType type; + + public KotlinSupplierSuspendWrapper(Object kotlinLambdaTarget, ResolvableType type, String functionName) { + this.name = functionName; + this.kotlinLambdaTarget = kotlinLambdaTarget; + this.type = type; + } + + public Object apply(Object input) { + if (ObjectUtils.isEmpty(input)) { + return this.get(); + } + return null; + } + + @Override + public Object get() { + return invoke(); + } + + @Override + public Object invoke() { + return CoroutinesUtils.invokeSuspendingSupplier(kotlinLambdaTarget); + } + + @Override + public ResolvableType getResolvableType() { + return type; + } + + @Override + public String getName() { + return this.name; + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/KotlinUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/KotlinUtils.java index e30f650df..0901fb9b7 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/KotlinUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/KotlinUtils.java @@ -16,9 +16,13 @@ package org.springframework.cloud.function.utils; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import org.springframework.cloud.function.context.config.TypeUtils; import org.springframework.core.KotlinDetector; /** @@ -29,10 +33,20 @@ private KotlinUtils() { } - public static boolean isKotlinType(Object object) { + public static boolean isKotlinType(Object object, Type functionType) { if (KotlinDetector.isKotlinPresent()) { - return KotlinDetector.isKotlinType(object.getClass()) || object instanceof Function0 + boolean isKotlinObject = KotlinDetector.isKotlinType(object.getClass()) + || object instanceof Function0 || object instanceof Function1; + if (isKotlinObject) { + return true; + } + // Check if there is a flow type in the functionType it will be converted to a Flux + else if (functionType instanceof ParameterizedType) { + Type[] types = ((ParameterizedType) functionType).getActualTypeArguments(); + return TypeUtils.hasFlowType(types); + } + return false; } return false; } diff --git a/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/CoroutinesUtils.kt b/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/CoroutinesUtils.kt index 8614b69db..d10bdad4d 100644 --- a/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/CoroutinesUtils.kt +++ b/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/CoroutinesUtils.kt @@ -22,111 +22,114 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactor.asFlux import kotlinx.coroutines.reactor.mono +import kotlinx.coroutines.suspendCancellableCoroutine import reactor.core.publisher.Flux -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.WildcardType import kotlin.coroutines.Continuation -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono /** * @author Adrien Poupard * */ - -fun getSuspendingFunctionArgType(type: Type): Type { - return getFlowTypeArguments(type) -} - -fun getFlowTypeArguments(type: Type): Type { - if(!isFlowType(type)) { - return type - } - val parameterizedLowerType = type as ParameterizedType - if(parameterizedLowerType.actualTypeArguments.isEmpty()) { - return parameterizedLowerType - } - - val actualTypeArgument = parameterizedLowerType.actualTypeArguments[0] - return if(actualTypeArgument is WildcardType) { - val wildcardTypeLower = parameterizedLowerType.actualTypeArguments[0] as WildcardType - wildcardTypeLower.upperBounds[0] - } else { - actualTypeArgument +private inline fun executeInCoroutineAndConvertToFlux(crossinline block: (Continuation) -> O): Flux { + return mono(Dispatchers.Unconfined) { + suspendCancellableCoroutine { continuation -> + try { + val result = block(continuation) + if (result != kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) { + continuation.resume(result) + } + } catch (e: Exception) { + continuation.resumeWithException(e) + } + } + }.flatMapMany { + it.convertToFlux() } } -fun isFlowType(type: Type): Boolean { - return type.typeName.startsWith(Flow::class.qualifiedName!!) -} - -fun getSuspendingFunctionReturnType(type: Type): Type { - val lower = getContinuationTypeArguments(type) - return getFlowTypeArguments(lower) -} - -fun isContinuationType(type: Type): Boolean { - return type.typeName.startsWith(Continuation::class.qualifiedName!!) -} - -fun isContinuationUnitType(type: Type): Boolean { - return isContinuationType(type) && type.typeName.contains(Unit::class.qualifiedName!!) -} - -fun isContinuationFlowType(type: Type): Boolean { - return isContinuationType(type) && type.typeName.contains(Flow::class.qualifiedName!!) -} - -private fun getContinuationTypeArguments(type: Type): Type { - if(!isContinuationType(type)) { - return type +/** + * Convert a value to a Flux, handling different types appropriately + * + * @param value The value to convert + * @return The value as a Flux + */ +private fun T?.convertToFlux(): Flux { + return when (this) { + is Flow<*> -> @Suppress("UNCHECKED_CAST") ((this as Flow).asFlux()) + is Flux<*> -> @Suppress("UNCHECKED_CAST") (this as Flux) + is Mono<*> -> @Suppress("UNCHECKED_CAST") (this.flatMapMany { Flux.just(it) } as Flux) + null -> Flux.empty() + else -> Flux.just(this) } - val parameterizedType = type as ParameterizedType - val wildcardType = parameterizedType.actualTypeArguments[0] as WildcardType - return wildcardType.lowerBounds[0] } -fun invokeSuspendingFunction(kotlinLambdaTarget: Any, arg0: Any): Flux { - val function = kotlinLambdaTarget as SuspendFunction - val flux = arg0 as Flux - return mono(Dispatchers.Unconfined) { - suspendCoroutineUninterceptedOrReturn> { - function.invoke(flux.asFlow(), it) +fun invokeSuspendingFlowFunction(kotlinLambdaTarget: Any, arg0: Flow): Flux { + try { + @Suppress("UNCHECKED_CAST") + val function = kotlinLambdaTarget as SuspendFunction, O> + return executeInCoroutineAndConvertToFlux { continuation -> + function.invoke(arg0, continuation) } - }.flatMapMany { - it.asFlux() + } catch (e: ClassCastException) { + throw IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.javaClass, e) } } -fun invokeSuspendingSupplier(kotlinLambdaTarget: Any): Flux { - val supplier = kotlinLambdaTarget as SuspendSupplier - return mono(Dispatchers.Unconfined) { - suspendCoroutineUninterceptedOrReturn> { - supplier.invoke(it) +fun invokeSuspendingSingleFunction(kotlinLambdaTarget: Any, arg0: I): Flux { + try { + @Suppress("UNCHECKED_CAST") + val function = kotlinLambdaTarget as SuspendFunction + return executeInCoroutineAndConvertToFlux { continuation -> + function.invoke(arg0, continuation) } - }.flatMapMany { - it.asFlux() + } catch (e: ClassCastException) { + throw IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.javaClass, e) } } -fun invokeSuspendingConsumer(kotlinLambdaTarget: Any, arg0: Any) { - val consumer = kotlinLambdaTarget as SuspendConsumer - val flux = arg0 as Flux - mono(Dispatchers.Unconfined) { - suspendCoroutineUninterceptedOrReturn { - consumer.invoke(flux.asFlow(), it) +fun invokeSuspendingSupplier(kotlinLambdaTarget: Any): Flux { + try { + @Suppress("UNCHECKED_CAST") + val supplier = kotlinLambdaTarget as SuspendSupplier + return executeInCoroutineAndConvertToFlux { continuation -> + supplier.invoke(continuation) } - }.subscribe() + } catch (e: ClassCastException) { + throw IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.javaClass, e) + } } -fun isValidSuspendingFunction(kotlinLambdaTarget: Any, arg0: Any): Boolean { - return arg0 is Flux<*> && kotlinLambdaTarget is Function2<*, *, *> +fun invokeSuspendingConsumer(kotlinLambdaTarget: Any, arg0: I) { + try { + @Suppress("UNCHECKED_CAST") + val consumer = kotlinLambdaTarget as SuspendConsumer + executeInCoroutineAndConvertToFlux { continuation -> + consumer.invoke(arg0, continuation) + }.subscribe() + } catch (e: ClassCastException) { + throw IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.javaClass, e) + } } -fun isValidSuspendingSupplier(kotlinLambdaTarget: Any): Boolean { - return kotlinLambdaTarget is Function1<*, *> +fun invokeSuspendingConsumerFlow(kotlinLambdaTarget: Any, arg0: Flux) { + try { + @Suppress("UNCHECKED_CAST") + val consumer = kotlinLambdaTarget as SuspendConsumer> + executeInCoroutineAndConvertToFlux { continuation -> + val flow = arg0.asFlow() + consumer.invoke(flow, continuation) + }.subscribe() + } catch (e: ClassCastException) { + throw IllegalArgumentException("Unsupported target type: " + kotlinLambdaTarget.javaClass, e) + } } -private typealias SuspendFunction = (Any?, Any?) -> Any? -private typealias SuspendConsumer = (Any?, Any?) -> Unit? -private typealias SuspendSupplier = (Any?) -> Any? +private typealias SuspendFunction = Function2, O> + +private typealias SuspendConsumer = Function2, Unit> + +private typealias SuspendSupplier = Function1, O> diff --git a/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/FunctionUtils.kt b/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/FunctionUtils.kt new file mode 100644 index 000000000..a449f1585 --- /dev/null +++ b/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/FunctionUtils.kt @@ -0,0 +1,67 @@ + +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("FunctionUtils") +package org.springframework.cloud.function.context.config + +import java.lang.reflect.Type +import java.util.function.Consumer +import java.util.function.Function +import java.util.function.Supplier + +fun isValidKotlinConsumer(functionType: Type, type: Array): Boolean { + return isTypeRepresentedByClass(functionType, Consumer::class.java) || ( + isTypeRepresentedByClass(functionType, Function1::class.java) && + type.size == 2 && + !isContinuationType(type[0]) && + (isUnitType(type[1]) || isVoidType(type[1])) + ) +} + +fun isValidKotlinSuspendConsumer(functionType: Type, type: Array): Boolean { + return isTypeRepresentedByClass(functionType, Function2::class.java) && type.size == 3 && + isContinuationUnitType(type[1]) +} + + +fun isValidKotlinFunction(functionType: Type, type: Array): Boolean { + return (isTypeRepresentedByClass(functionType, Function1::class.java) || + isTypeRepresentedByClass(functionType, Function::class.java)) && + type.size == 2 && !isContinuationType(type[0]) && + !isUnitType(type[1]) +} + + +fun isValidKotlinSuspendFunction(functionType: Type, type: Array): Boolean { + return isTypeRepresentedByClass(functionType, Function2::class.java) && type.size == 3 && + isContinuationType(type[1]) && + !isContinuationUnitType(type[1]) +} + +fun isValidKotlinSupplier(functionType: Type): Boolean { + return isTypeRepresentedByClass(functionType, Function0::class.java) || + isTypeRepresentedByClass(functionType, Supplier::class.java) +} + +fun isValidKotlinSuspendSupplier(functionType: Type, type: Array): Boolean { + return isTypeRepresentedByClass(functionType, Function1::class.java) && type.size == 2 && + isContinuationType(type[0]) +} + +fun isTypeRepresentedByClass(type: Type, clazz: Class<*>): Boolean { + return type.typeName.contains(clazz.name) +} diff --git a/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/TypeUtils.kt b/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/TypeUtils.kt new file mode 100644 index 000000000..42d44bb2b --- /dev/null +++ b/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/TypeUtils.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2021-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("TypeUtils") +package org.springframework.cloud.function.context.config + +import kotlinx.coroutines.flow.Flow +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.WildcardType +import kotlin.coroutines.Continuation +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactor.asFlux +import org.springframework.core.ResolvableType +import reactor.core.publisher.Flux + +/** + * @author Adrien Poupard + * + */ +fun getSuspendingFunctionArgType(type: Type): ResolvableType { + return ResolvableType.forType(getFlowTypeArguments(type)) +} + +fun getSuspendingFunctionReturnType(type: Type): ResolvableType { + val lower = getContinuationTypeArguments(type) + return ResolvableType.forType(getFlowTypeArguments(lower)) +} + +fun getFlowTypeArguments(type: Type): Type { + if(!isFlowType(type)) { + return type + } + val parameterizedLowerType = type as ParameterizedType + if(parameterizedLowerType.actualTypeArguments.isEmpty()) { + return parameterizedLowerType + } + + val actualTypeArgument = parameterizedLowerType.actualTypeArguments[0] + return if(actualTypeArgument is WildcardType) { + val wildcardTypeLower = parameterizedLowerType.actualTypeArguments[0] as WildcardType + wildcardTypeLower.upperBounds[0] + } else { + actualTypeArgument + } +} + +fun hasFlowType(types: Array) : Boolean { + return types.any { isFlowType(it) } +} + +fun isFlowType(type: Type): Boolean { + return type.typeName.startsWith(Flow::class.qualifiedName!!) +} + +fun isContinuationType(type: Type): Boolean { + return type.typeName.startsWith(Continuation::class.qualifiedName!!) +} + +fun isUnitType(type: Type): Boolean { + return isTypeRepresentedByClass(type, Unit::class.java) +} + +fun isVoidType(type: Type): Boolean { + return isTypeRepresentedByClass(type, Void::class.java) +} + +fun isContinuationUnitType(type: Type): Boolean { + return isContinuationType(type) && type.typeName.contains(Unit::class.qualifiedName!!) +} + +fun isContinuationFlowType(type: Type): Boolean { + return isContinuationType(type) && type.typeName.contains(Flow::class.qualifiedName!!) +} + +internal fun getContinuationTypeArguments(type: Type): Type { + if(!isContinuationType(type)) { + return type + } + val parameterizedType = type as ParameterizedType + return when (val typeArg = parameterizedType.actualTypeArguments[0]) { + is WildcardType -> typeArg.lowerBounds[0] + is ParameterizedType -> typeArg + else -> typeArg + } +} + +fun convertToFlow(arg0: Flux): Flow { + return arg0.asFlow() +} + +fun convertToFlux(arg0: Flow): Flux { + return arg0.asFlux() +} diff --git a/spring-cloud-function-context/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-function-context/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 90873e09a..169527284 100644 --- a/spring-cloud-function-context/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-function-context/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1,4 @@ org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration org.springframework.cloud.function.cloudevent.CloudEventsFunctionExtensionConfiguration -org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration org.springframework.cloud.function.context.config.FunctionsEndpointAutoConfiguration org.springframework.cloud.function.observability.ObservationAutoConfiguration diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ConsumerArityCatalogueTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ConsumerArityCatalogueTests.java new file mode 100644 index 000000000..16ba4caf9 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ConsumerArityCatalogueTests.java @@ -0,0 +1,281 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for different arity functions, suppliers, and consumers in the FunctionCatalog. + * + * @author Adrien Poupard + */ +public class ConsumerArityCatalogueTests { + + private GenericApplicationContext context; + + private FunctionCatalog catalog; + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "consumerPlain", "consumerKotlinPlain", "consumerJavaPlain" + }) + public void testPlainConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + // Plain string consumer + String typeName = consumer.getInputType().getTypeName(); + assertThat(typeName).isEqualTo("java.lang.String"); + + // Just verifying it doesn't throw an exception + consumer.apply("test"); + } + + @ParameterizedTest + @ValueSource(strings = {"consumerSuspendPlain", "consumerKotlinSuspendPlain"}) + public void testSuspendPlainConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Suspend Plain consumer + assertThat(typeName).isEqualTo("java.lang.String"); + consumer.apply("test"); + } + + @ParameterizedTest + @ValueSource(strings = {"consumerJavaFlow", "consumerKotlinFlow", "consumerFlow"}) + public void testFlowConsumerMethods(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Flow consumer + // Note: Spring Cloud Function might convert Kotlin Flow to Reactor Flux + assertThat(typeName.contains("Flux")).isTrue(); + // We can't easily create a Flow instance for testing, so we just verify the type + } + + @ParameterizedTest + @ValueSource(strings = {"consumerMonoInput", "consumerJavaMonoInput", "consumerKotlinMonoInput"}) + public void testMonoInputConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // MonoInput consumer (actually a String consumer that returns Mono) + assertThat(typeName).isEqualTo("java.lang.String"); + consumer.apply("test"); + } + + @ParameterizedTest + @ValueSource(strings = {"consumerMono", "consumerJavaMono", "consumerKotlinMono"}) + public void testMonoConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Mono consumer + assertThat(typeName).contains("Mono"); + // We can't easily test the actual consumption of a Mono, so we just verify the type + } + + @ParameterizedTest + @ValueSource(strings = {"consumerFlux", "consumerJavaFlux", "consumerKotlinFlux"}) + public void testFluxConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Flux consumer + assertThat(typeName).contains("Flux"); + // We can't easily test the actual consumption of a Flux, so we just verify the type + } + + @ParameterizedTest + @ValueSource(strings = {"consumerMessage", "consumerJavaMessage", "consumerKotlinMessage"}) + public void testMessageConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Message consumer + assertThat(typeName).contains("Message"); + Message message = MessageBuilder.withPayload("test").build(); + consumer.apply(message); + } + + @ParameterizedTest + @ValueSource(strings = {"consumerMonoMessage", "consumerJavaMonoMessage", "consumerKotlinMonoMessage"}) + public void testMonoMessageConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Mono consumer + assertThat(typeName).contains("Mono"); + assertThat(typeName).contains("Message"); + // We can't easily test the actual consumption of a Mono, so we just verify the type + } + + @ParameterizedTest + @ValueSource(strings = {"consumerSuspendMessage", "consumerKotlinSuspendMessage"}) + public void testSuspendMessageConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Suspend Message consumer + assertThat(typeName).contains("Message"); + Message message = MessageBuilder.withPayload("test").build(); + consumer.apply(message); + } + + @ParameterizedTest + @ValueSource(strings = {"consumerFluxMessage", "consumerJavaFluxMessage", "consumerKotlinFluxMessage"}) + public void testFluxMessageConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + // Flux consumer + assertThat(typeName).contains("Flux"); + assertThat(typeName).contains("Message"); + } + + @ParameterizedTest + @ValueSource(strings = {"consumerSuspendFlowMessage", "consumerKotlinSuspendFlowMessage"}) + public void testSuspendFlowMessageConsumers(String consumerName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper consumer = this.catalog.lookup(consumerName); + + // Test should fail if consumer is not found + assertThat(consumer).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(consumer.isConsumer()).isTrue(); + + String typeName = consumer.getInputType().getTypeName(); + + assertThat(typeName.contains("Flux")).isTrue(); + assertThat(typeName).contains("Message"); + } + + private void create(Class[] types, String... props) { + this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run(); + this.catalog = this.context.getBean(FunctionCatalog.class); + } +} diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/FunctionArityCatalogueTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/FunctionArityCatalogueTests.java new file mode 100644 index 000000000..6019d8efb --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/FunctionArityCatalogueTests.java @@ -0,0 +1,513 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for different arity functions in the FunctionCatalog. + * + * @author Adrien Poupard + */ +public class FunctionArityCatalogueTests { + + private GenericApplicationContext context; + + private FunctionCatalog catalog; + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToPlain", "functionJavaPlainToPlain", "functionKotlinPlainToPlain" + }) + public void testPlainToPlainFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Plain string to int function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName).isEqualTo("java.lang.String"); + assertThat(outputTypeName).isEqualTo("java.lang.Integer"); + + // Verify function execution + Object result = function.apply("test"); + assertThat(result).isEqualTo(4); // "test".length() == 4 + } + + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToFlow", "functionJavaPlainToFlow", "functionKotlinPlainToFlow" + }) + public void testPlainToFlowFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Plain string to flow function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName).isEqualTo("java.lang.String"); + // Output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(outputTypeName.contains("Flux")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionFlowToPlain", "functionJavaFlowToPlain", "functionKotlinFlowToPlain" + }) + public void testFlowToPlainFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Flow to plain function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + // Input might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName).isEqualTo("java.lang.Integer"); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionFlowToFlow", "functionJavaFlowToFlow", "functionKotlinFlowToFlow" + }) + public void testFlowToFlowFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Flow to flow function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + // Input and output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionSuspendPlainToPlain", "functionKotlinSuspendPlainToPlain" + }) + public void testSuspendPlainToPlainFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Suspend plain to plain function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName).isEqualTo("java.lang.String"); + // Suspend functions are wrapped in Flux by Spring Cloud Function + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Integer")).isTrue(); + + // Verify function execution + Object result = function.apply("test"); + // Result is a Flux, so we can't directly assert on the value + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionSuspendPlainToFlow", "functionKotlinSuspendPlainToFlow" + }) + public void testSuspendPlainToFlowFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Suspend plain to flow function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName).isEqualTo("java.lang.String"); + // Output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(outputTypeName.contains("Flux")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionSuspendFlowToPlain", "functionKotlinSuspendFlowToPlain" + }) + public void testSuspendFlowToPlainFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Suspend flow to plain function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + // Input might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName).isEqualTo("java.lang.Integer"); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionSuspendFlowToFlow", "functionKotlinSuspendFlowToFlow" + }) + public void testSuspendFlowToFlowFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Suspend flow to flow function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + // Input and output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToMono", "functionJavaPlainToMono", "functionKotlinPlainToMono" + }) + public void testPlainToMonoFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Plain to mono function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName).isEqualTo("java.lang.String"); + assertThat(outputTypeName.contains("Mono")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToFlux", "functionJavaPlainToFlux", "functionKotlinPlainToFlux" + }) + public void testPlainToFluxFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Plain to flux function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName).isEqualTo("java.lang.String"); + assertThat(outputTypeName.contains("Flux")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionMonoToMono", "functionJavaMonoToMono", "functionKotlinMonoToMono" + }) + public void testMonoToMonoFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Mono to mono function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName.contains("Mono")).isTrue(); + assertThat(outputTypeName.contains("Mono")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionFluxToFlux", "functionJavaFluxToFlux", "functionKotlinFluxToFlux" + }) + public void testFluxToFluxFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Flux to flux function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionFluxToMono", "functionJavaFluxToMono", "functionKotlinFluxToMono" + }) + public void testFluxToMonoFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Flux to mono function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Mono")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionMessageToMessage", "functionJavaMessageToMessage", "functionKotlinMessageToMessage" + }) + public void testMessageToMessageFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Message to message function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName.contains("Message")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify function execution with a message + Message message = MessageBuilder.withPayload("test").build(); + Object result = function.apply(message); + assertThat(result).isInstanceOf(Message.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionSuspendMessageToMessage", "functionKotlinSuspendMessageToMessage" + }) + public void testSuspendMessageToMessageFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Suspend message to message function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName.contains("Message")).isTrue(); + // Suspend functions are wrapped in Flux by Spring Cloud Function + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify function execution with a message + Message message = MessageBuilder.withPayload("test").build(); + Object result = function.apply(message); + // Result is a Flux, so we can't directly assert it's a Message + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionMonoMessageToMonoMessage", "functionJavaMonoMessageToMonoMessage", "functionKotlinMonoMessageToMonoMessage" + }) + public void testMonoMessageToMonoMessageFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Mono message to mono message function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName.contains("Mono")).isTrue(); + assertThat(inputTypeName.contains("Message")).isTrue(); + assertThat(outputTypeName.contains("Mono")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionFluxMessageToFluxMessage", "functionJavaFluxMessageToFluxMessage", "functionKotlinFluxMessageToFluxMessage" + }) + public void testFluxMessageToFluxMessageFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Flux message to flux message function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(inputTypeName.contains("Message")).isTrue(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionFlowMessageToFlowMessage", "functionJavaFlowMessageToFlowMessage", "functionKotlinFlowMessageToFlowMessage" + }) + public void testFlowMessageToFlowMessageFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Flow message to flow message function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + // Input and output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(inputTypeName.contains("Message")).isTrue(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { + "functionSuspendFlowMessageToFlowMessage", "functionKotlinSuspendFlowMessageToFlowMessage" + }) + public void testSuspendFlowMessageToFlowMessageFunctions(String functionName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper function = this.catalog.lookup(functionName); + + // Test should fail if function is not found + assertThat(function).as("Function not found: " + functionName).isNotNull(); + + // Verify it's a function + assertThat(function.isFunction()).isTrue(); + + // Suspend flow message to flow message function + String inputTypeName = function.getInputType().getTypeName(); + String outputTypeName = function.getOutputType().getTypeName(); + // Input and output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(inputTypeName.contains("Flux")).isTrue(); + assertThat(inputTypeName.contains("Message")).isTrue(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + } + + private void create(Class[] types, String... props) { + this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run(); + this.catalog = this.context.getBean(FunctionCatalog.class); + } +} diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinConsumerArityNativeCastTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinConsumerArityNativeCastTests.java new file mode 100644 index 000000000..8ff89580b --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinConsumerArityNativeCastTests.java @@ -0,0 +1,366 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin; + +import java.util.function.Consumer; + +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to verify that Kotlin Consumer implementations can be cast to native Java functional interfaces, + * invoked properly, and produce correct results. + * + * @author AI Assistant + */ +public class KotlinConsumerArityNativeCastTests { + + private GenericApplicationContext context; + + private FunctionCatalog catalog; + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + /** + * Test that plain consumers from KotlinConsumerArityBean, KotlinConsumerArityComponent, and KotlinConsumerArityJava + * can be cast to java.util.function.Consumer and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerPlain", "consumerKotlinPlain", "consumerJavaPlain" + }) + public void testPlainConsumersCastToNative(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to java.util.function.Consumer + @SuppressWarnings("unchecked") + Consumer consumer = (Consumer) consumerBean; + + // Invoke the consumer + consumer.accept("test-native-cast"); + + // Since consumers don't return values, we can only verify they don't throw exceptions + // In a real-world scenario, you might verify side effects like logging or database changes + } + + /** + * Test that Mono-returning consumers can be invoked through the FunctionInvocationWrapper. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerMonoInput", "consumerKotlinMonoInput", "consumerJavaMonoInput" + }) + public void testMonoInputConsumersInvocation(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(wrapper.isConsumer()).isTrue(); + + // Verify input type + String typeName = wrapper.getInputType().getTypeName(); + assertThat(typeName).isEqualTo("java.lang.String"); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to java.util.function.Consumer + @SuppressWarnings("unchecked") + Consumer consumer = (Consumer) consumerBean; + + // Invoke the consumer + consumer.accept("test-mono-input"); + // No exception means success + } + + /** + * Test that Message consumers can be cast to java.util.function.Consumer and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerMessage", "consumerKotlinMessage", "consumerJavaMessage" + }) + public void testMessageConsumersCastToNative(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to java.util.function.Consumer + @SuppressWarnings("unchecked") + Consumer> consumer = (Consumer>) consumerBean; + + // Create a message and invoke the consumer + Message message = MessageBuilder.withPayload("test-message-cast").build(); + consumer.accept(message); + + // Since consumers don't return values, we can only verify they don't throw exceptions + } + + /** + * Test that Flux consumers can be invoked through the FunctionInvocationWrapper. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerFlux", "consumerKotlinFlux", "consumerJavaFlux" + }) + public void testFluxConsumersInvocation(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(wrapper.isConsumer()).isTrue(); + + // Verify input type (should be Flux) + String typeName = wrapper.getInputType().getTypeName(); + assertThat(typeName.contains("Flux")).isTrue(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to java.util.function.Consumer + @SuppressWarnings("unchecked") + Consumer> consumer = (Consumer>) consumerBean; + + // We can't easily create a Flux here, but we can verify the cast works + assertThat(consumer).isNotNull(); + } + + /** + * Test that Flow consumers can be cast to java.util.function.Consumer and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerFlow", "consumerKotlinFlow", "consumerJavaFlow" + }) + public void testFlowConsumersCastToNative(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to java.util.function.Consumer + @SuppressWarnings("unchecked") + Consumer> consumer = (Consumer>) consumerBean; + + // We can't easily create a Flow here, but we can verify the cast works + assertThat(consumer).isNotNull(); + } + + /** + * Test that suspend consumers can be invoked through the FunctionInvocationWrapper. + * Note: Suspend functions can't be directly cast to Java functional interfaces, + * but they can be invoked through the wrapper. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerSuspendPlain", "consumerKotlinSuspendPlain" + }) + public void testSuspendConsumersInvocation(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(wrapper.isConsumer()).isTrue(); + + // Verify input type + String typeName = wrapper.getInputType().getTypeName(); + assertThat(typeName).isEqualTo("java.lang.String"); + + // Invoke through the wrapper + wrapper.apply("test-suspend"); + // No exception means success + } + + /** + * Test that suspend flow consumers can be invoked through the FunctionInvocationWrapper. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerSuspendFlow", "consumerKotlinSuspendFlow" + }) + public void testSuspendFlowConsumersInvocation(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Verify it's a consumer + assertThat(wrapper.isConsumer()).isTrue(); + + // Verify input type (should be converted to Flux) + String typeName = wrapper.getInputType().getTypeName(); + assertThat(typeName.contains("Flux")).isTrue(); + + // We can't easily create a Flow/Flux here for testing + } + + /** + * Test that Message Flow consumers can be cast to java.util.function.Consumer. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerFlowMessage", "consumerKotlinFlowMessage", "consumerJavaFlowMessage" + }) + public void testFlowMessageConsumersCastToNative(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to java.util.function.Consumer + @SuppressWarnings("unchecked") + Consumer>> consumer = (Consumer>>) consumerBean; + + // We can't easily create a Flow here, but we can verify the cast works + assertThat(consumer).isNotNull(); + } + + /** + * Test that plain consumers can be cast to kotlin.jvm.functions.Function1 and invoked. + * This verifies that Consumer implementations also implement the Kotlin Function1 interface. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerPlain", "consumerKotlinPlain", "consumerJavaPlain" + }) + public void testPlainConsumersCastToKotlinFunction1(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function1 + @SuppressWarnings("unchecked") + Function1 consumer = (Function1) consumerBean; + + // Invoke the consumer + consumer.invoke("test-kotlin-cast"); + + // Since consumers don't return values, we can only verify they don't throw exceptions + } + + /** + * Test that Message consumers can be cast to kotlin.jvm.functions.Function1 and invoked. + * This verifies that Consumer implementations also implement the Kotlin Function1 interface. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerMessage", "consumerKotlinMessage" + }) + public void testMessageConsumersCastToKotlinFunction1(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function1 + @SuppressWarnings("unchecked") + Function1, Void> consumer = (Function1, Void>) consumerBean; + + // Create a message and invoke the consumer + Message message = MessageBuilder.withPayload("test-kotlin-message-cast").build(); + consumer.invoke(message); + + // Since consumers don't return values, we can only verify they don't throw exceptions + } + + /** + * Test that Flow consumers can be cast to kotlin.jvm.functions.Function1. + * This verifies that Consumer implementations also implement the Kotlin Function1 interface. + */ + @ParameterizedTest + @ValueSource(strings = { + "consumerFlow", "consumerKotlinFlow" + }) + public void testFlowConsumersCastToKotlinFunction1(String consumerName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(consumerName); + assertThat(wrapper).as("Consumer not found: " + consumerName).isNotNull(); + + // Get the actual consumer bean + Object consumerBean = wrapper.getTarget(); + assertThat(consumerBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function1 + @SuppressWarnings("unchecked") + Function1, Void> consumer = (Function1, Void>) consumerBean; + + // We can't easily create a Flow here, but we can verify the cast works + assertThat(consumer).isNotNull(); + } + + private void create(Class[] types, String... props) { + this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run(); + this.catalog = this.context.getBean(FunctionCatalog.class); + } +} diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinFunctionArityNativeCastTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinFunctionArityNativeCastTests.java new file mode 100644 index 000000000..da9632db1 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinFunctionArityNativeCastTests.java @@ -0,0 +1,443 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin; + +import java.util.function.Function; + +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to verify that Kotlin Function implementations can be cast to native Java functional interfaces, + * invoked properly, and produce correct results. + * + * @author AI Assistant + */ +public class KotlinFunctionArityNativeCastTests { + + private GenericApplicationContext context; + + private FunctionCatalog catalog; + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + /** + * Test that plain functions from KotlinFunctionArityBean, KotlinFunctionArityComponent, and KotlinFunctionArityJava + * can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToPlain", "functionKotlinPlainToPlain", "functionJavaPlainToPlain" + }) + public void testPlainFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function function = (Function) functionBean; + + // Invoke the function + Integer result = function.apply("test-native-cast"); + + // Verify the result (should be the length of the input string) + assertThat(result).isEqualTo("test-native-cast".length()); + } + + /** + * Test that functions returning Mono can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToMono", "functionKotlinPlainToMono", "functionJavaPlainToMono" + }) + public void testMonoFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function> function = (Function>) functionBean; + + // Invoke the function + Mono result = function.apply("test-mono"); + + // Verify the result + Integer value = result.block(); + assertThat(value).isEqualTo("test-mono".length()); + } + + /** + * Test that functions returning Flux can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToFlux", "functionKotlinPlainToFlux", "functionJavaPlainToFlux" + }) + public void testFluxFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function> function = (Function>) functionBean; + + // Invoke the function + Flux result = function.apply("abc"); + + // Verify the result + assertThat(result.collectList().block()).containsExactly("a", "b", "c"); + } + + /** + * Test that Mono to Mono functions can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionMonoToMono", "functionKotlinMonoToMono", "functionJavaMonoToMono" + }) + public void testMonoToMonoFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function, Mono> function = (Function, Mono>) functionBean; + + // Invoke the function + Mono result = function.apply(Mono.just("test-mono-to-mono")); + + // Verify the result + String value = result.block(); + assertThat(value).isEqualTo("TEST-MONO-TO-MONO"); + } + + /** + * Test that Flux to Flux functions can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionFluxToFlux", "functionKotlinFluxToFlux", "functionJavaFluxToFlux" + }) + public void testFluxToFluxFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function, Flux> function = (Function, Flux>) functionBean; + + // Invoke the function + Flux result = function.apply(Flux.just("a", "bb", "ccc")); + + // Verify the result + assertThat(result.collectList().block()).containsExactly(1, 2, 3); + } + + /** + * Test that Message to Message functions can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionMessageToMessage", "functionKotlinMessageToMessage", "functionJavaMessageToMessage" + }) + public void testMessageToMessageFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function, Message> function = (Function, Message>) functionBean; + + // Create a message and invoke the function + Message message = MessageBuilder.withPayload("test-message").build(); + Message result = function.apply(message); + + // Verify the result + assertThat(result.getPayload()).isEqualTo("test-message".length()); + assertThat(result.getHeaders().get("processed")).isEqualTo("true"); + } + + /** + * Test that Mono<Message> to Mono<Message> functions can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionMonoMessageToMonoMessage", "functionKotlinMonoMessageToMonoMessage", "functionJavaMonoMessageToMonoMessage" + }) + public void testMonoMessageToMonoMessageFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function>, Mono>> function = + (Function>, Mono>>) functionBean; + + // Create a message and invoke the function + Message message = MessageBuilder.withPayload("test-mono-message").build(); + Mono> result = function.apply(Mono.just(message)); + + // Verify the result + Message resultMessage = result.block(); + assertThat(resultMessage).isNotNull(); + assertThat(resultMessage.getHeaders().get("mono-processed")).isEqualTo("true"); + } + + /** + * Test that Flux<Message> to Flux<Message> functions can be cast to java.util.function.Function and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionFluxMessageToFluxMessage", "functionKotlinFluxMessageToFluxMessage", "functionJavaFluxMessageToFluxMessage" + }) + public void testFluxMessageToFluxMessageFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function>, Flux>> function = + (Function>, Flux>>) functionBean; + + // Create messages and invoke the function + Message message1 = MessageBuilder.withPayload("test1").build(); + Message message2 = MessageBuilder.withPayload("test2").build(); + Flux> result = function.apply(Flux.just(message1, message2)); + + // Verify the result + assertThat(result.collectList().block()).hasSize(2); + assertThat(result.map(msg -> msg.getPayload()).collectList().block()) + .containsExactly("TEST1", "TEST2"); + } + + /** + * Test that Flow<Message> to Flow<Message> functions can be cast to java.util.function.Function. + * Note: We can't easily invoke Flow functions directly in Java, but we can verify the cast works. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionFlowMessageToFlowMessage", "functionKotlinFlowMessageToFlowMessage", "functionJavaFlowMessageToFlowMessage" + }) + public void testFlowMessageToFlowMessageFunctionsCastToNative(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to java.util.function.Function + @SuppressWarnings("unchecked") + Function>, Flow>> function = + (Function>, Flow>>) functionBean; + + // Verify the cast works + assertThat(function).isNotNull(); + } + + /** + * Test that plain functions can be cast to kotlin.jvm.functions.Function1 and invoked. + * This verifies that Function implementations also implement the Kotlin Function1 interface. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToPlain", "functionKotlinPlainToPlain" + }) + public void testPlainFunctionsCastToKotlinFunction1(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function1 + @SuppressWarnings("unchecked") + Function1 function = (Function1) functionBean; + + // Invoke the function + Integer result = function.invoke("test-kotlin-cast"); + + // Verify the result (should be the length of the input string) + assertThat(result).isEqualTo("test-kotlin-cast".length()); + } + + /** + * Test that functions returning Mono can be cast to kotlin.jvm.functions.Function1 and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToMono", "functionKotlinPlainToMono" + }) + public void testMonoFunctionsCastToKotlinFunction1(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function1 + @SuppressWarnings("unchecked") + Function1> function = (Function1>) functionBean; + + // Invoke the function + Mono result = function.invoke("test-kotlin-mono"); + + // Verify the result + Integer value = result.block(); + assertThat(value).isEqualTo("test-kotlin-mono".length()); + } + + /** + * Test that functions returning Flux can be cast to kotlin.jvm.functions.Function1 and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionPlainToFlux", "functionKotlinPlainToFlux" + }) + public void testFluxFunctionsCastToKotlinFunction1(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function1 + @SuppressWarnings("unchecked") + Function1> function = (Function1>) functionBean; + + // Invoke the function + Flux result = function.invoke("abc"); + + // Verify the result + assertThat(result.collectList().block()).containsExactly("a", "b", "c"); + } + + /** + * Test that Message to Message functions can be cast to kotlin.jvm.functions.Function1 and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "functionMessageToMessage", "functionKotlinMessageToMessage" + }) + public void testMessageFunctionsCastToKotlinFunction1(String functionName) { + create(new Class[] {KotlinArityApplication.class}); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(functionName); + assertThat(wrapper).as("Function not found: " + functionName).isNotNull(); + + // Get the actual function bean + Object functionBean = wrapper.getTarget(); + assertThat(functionBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function1 + @SuppressWarnings("unchecked") + Function1, Message> function = (Function1, Message>) functionBean; + + // Create a message and invoke the function + Message message = MessageBuilder.withPayload("test-kotlin-message").build(); + Message result = function.invoke(message); + + // Verify the result + assertThat(result.getPayload()).isEqualTo("test-kotlin-message".length()); + assertThat(result.getHeaders().get("processed")).isEqualTo("true"); + } + + private void create(Class[] types, String... props) { + this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run(); + this.catalog = this.context.getBean(FunctionCatalog.class); + } +} diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinSupplierArityNativeCastTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinSupplierArityNativeCastTests.java new file mode 100644 index 000000000..d6111e186 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinSupplierArityNativeCastTests.java @@ -0,0 +1,379 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin; + +import java.util.function.Supplier; + +import kotlin.jvm.functions.Function0; +import kotlinx.coroutines.flow.Flow; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to verify that Kotlin Supplier implementations can be cast to native Java functional interfaces, + * invoked properly, and produce correct results. + * + * @author AI Assistant + */ +public class KotlinSupplierArityNativeCastTests { + + private GenericApplicationContext context; + + private FunctionCatalog catalog; + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + /** + * Test that plain suppliers from KotlinSupplierArityBean, KotlinAritySupplierComponent, and KotlinSupplierArityJava + * can be cast to java.util.function.Supplier and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierPlain", "supplierKotlinPlain", "supplierJavaPlain" + }) + public void testPlainSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier supplier = (Supplier) supplierBean; + + // Invoke the supplier + Integer result = supplier.get(); + + // Verify the result (should be 42 based on the implementation) + assertThat(result).isEqualTo(42); + } + + /** + * Test that Mono suppliers can be cast to java.util.function.Supplier and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierMono", "supplierKotlinMono", "supplierJavaMono" + }) + public void testMonoSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier> supplier = (Supplier>) supplierBean; + + // Invoke the supplier + Mono result = supplier.get(); + + // Verify the result + String value = result.block(); + assertThat(value).isEqualTo("Hello from Mono"); + } + + /** + * Test that Flux suppliers can be cast to java.util.function.Supplier and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierFlux", "supplierKotlinFlux", "supplierJavaFlux" + }) + public void testFluxSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier> supplier = (Supplier>) supplierBean; + + // Invoke the supplier + Flux result = supplier.get(); + + // Verify the result + assertThat(result.collectList().block()).containsExactly("Alpha", "Beta", "Gamma"); + } + + /** + * Test that Message suppliers can be cast to java.util.function.Supplier and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierMessage", "supplierKotlinMessage", "supplierJavaMessage" + }) + public void testMessageSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier> supplier = (Supplier>) supplierBean; + + // Invoke the supplier + Message result = supplier.get(); + + // Verify the result + assertThat(result.getPayload()).isEqualTo("Hello from Message"); + assertThat(result.getHeaders().get("messageId")).isNotNull(); + } + + /** + * Test that Mono<Message> suppliers can be cast to java.util.function.Supplier and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierMonoMessage", "supplierKotlinMonoMessage", "supplierJavaMonoMessage" + }) + public void testMonoMessageSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier>> supplier = (Supplier>>) supplierBean; + + // Invoke the supplier + Mono> result = supplier.get(); + + // Verify the result + Message message = result.block(); + assertThat(message).isNotNull(); + assertThat(message.getPayload()).isEqualTo("Hello from Mono Message"); + assertThat(message.getHeaders().get("monoMessageId")).isNotNull(); + assertThat(message.getHeaders().get("source")).isEqualTo("mono"); + } + + /** + * Test that Flux<Message> suppliers can be cast to java.util.function.Supplier and invoked. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierFluxMessage", "supplierKotlinFluxMessage", "supplierJavaFluxMessage" + }) + public void testFluxMessageSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier>> supplier = (Supplier>>) supplierBean; + + // Invoke the supplier + Flux> result = supplier.get(); + + // Verify the result + assertThat(result.collectList().block()).hasSize(2); + assertThat(result.map(msg -> msg.getPayload()).collectList().block()) + .containsExactly("Msg1", "Msg2"); + } + + /** + * Test that Flow suppliers can be cast to java.util.function.Supplier. + * Note: We can't easily invoke Flow suppliers directly in Java, but we can verify the cast works. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierFlow", "supplierKotlinFlow", "supplierJavaFlow" + }) + public void testFlowSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier> supplier = (Supplier>) supplierBean; + + // Verify the cast works + assertThat(supplier).isNotNull(); + } + + /** + * Test that Flow<Message> suppliers can be cast to java.util.function.Supplier. + * Note: We can't easily invoke Flow suppliers directly in Java, but we can verify the cast works. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierFlowMessage", "supplierKotlinFlowMessage", "supplierJavaFlowMessage" + }) + public void testFlowMessageSuppliersCastToNative(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to java.util.function.Supplier + @SuppressWarnings("unchecked") + Supplier>> supplier = (Supplier>>) supplierBean; + + // Verify the cast works + assertThat(supplier).isNotNull(); + } + + /** + * Test that plain suppliers can be cast to kotlin.jvm.functions.Function0 and invoked. + * This verifies that Supplier implementations also implement the Kotlin Function0 interface. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierPlain", "supplierKotlinPlain" + }) + public void testPlainSuppliersCastToKotlinFunction0(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function0 + @SuppressWarnings("unchecked") + Function0 supplier = (Function0) supplierBean; + + // Invoke the supplier + Integer result = supplier.invoke(); + + // Verify the result (should be 42 based on the implementation) + assertThat(result).isEqualTo(42); + } + + /** + * Test that Mono suppliers can be cast to kotlin.jvm.functions.Function0 and invoked. + * This verifies that Supplier implementations also implement the Kotlin Function0 interface. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierMono", "supplierKotlinMono" + }) + public void testMonoSuppliersCastToKotlinFunction0(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function0 + @SuppressWarnings("unchecked") + Function0> supplier = (Function0>) supplierBean; + + // Invoke the supplier + Mono result = supplier.invoke(); + + // Verify the result + String value = result.block(); + assertThat(value).isEqualTo("Hello from Mono"); + } + + /** + * Test that Message suppliers can be cast to kotlin.jvm.functions.Function0 and invoked. + * This verifies that Supplier implementations also implement the Kotlin Function0 interface. + */ + @ParameterizedTest + @ValueSource(strings = { + "supplierMessage", "supplierKotlinMessage" + }) + public void testMessageSuppliersCastToKotlinFunction0(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper wrapper = this.catalog.lookup(supplierName); + assertThat(wrapper).as("Supplier not found: " + supplierName).isNotNull(); + + // Get the actual supplier bean + Object supplierBean = wrapper.getTarget(); + assertThat(supplierBean).isNotNull(); + + // Cast to kotlin.jvm.functions.Function0 + @SuppressWarnings("unchecked") + Function0> supplier = (Function0>) supplierBean; + + // Invoke the supplier + Message result = supplier.invoke(); + + // Verify the result + assertThat(result.getPayload()).isEqualTo("Hello from Message"); + assertThat(result.getHeaders().get("messageId")).isNotNull(); + } + + private void create(Class[] types, String... props) { + this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run(); + this.catalog = this.context.getBean(FunctionCatalog.class); + } +} diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/SupplierArityCatalogueTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/SupplierArityCatalogueTests.java new file mode 100644 index 000000000..9bb0e8786 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/SupplierArityCatalogueTests.java @@ -0,0 +1,354 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for different arity suppliers in the FunctionCatalog. + * + * @author Adrien Poupard + */ +public class SupplierArityCatalogueTests { + + private GenericApplicationContext context; + + private FunctionCatalog catalog; + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierPlain", "supplierJavaPlain", "supplierKotlinPlain" + }) + public void testPlainSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Plain supplier returns Int/Integer + String outputTypeName = supplier.getOutputType().getTypeName(); + assertThat(outputTypeName).isEqualTo("java.lang.Integer"); + + // Verify supplier execution + Object result = supplier.get(); + assertThat(result).isEqualTo(42); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierFlow", "supplierJavaFlow", "supplierKotlinFlow" + }) + public void testFlowSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Flow supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + // Output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(outputTypeName.contains("Flux")).isTrue(); + + // Verify supplier execution returns a Flow/Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierSuspendPlain", "supplierKotlinSuspendPlain" + }) + public void testSuspendPlainSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Suspend plain supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + // Suspend functions are wrapped in Flux by Spring Cloud Function + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("String")).isTrue(); + + // Verify supplier execution returns a Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierSuspendFlow", "supplierKotlinSuspendFlow" + }) + public void testSuspendFlowSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Suspend flow supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + // Output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(outputTypeName.contains("Flux")).isTrue(); + + // Verify supplier execution returns a Flow/Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierMono", "supplierJavaMono", "supplierKotlinMono" + }) + public void testMonoSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Mono supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + assertThat(outputTypeName.contains("Mono")).isTrue(); + + // Verify supplier execution returns a Mono + Object result = supplier.get(); + assertThat(result.toString()).contains("Mono"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierFlux", "supplierJavaFlux", "supplierKotlinFlux" + }) + public void testFluxSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Flux supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + + // Verify supplier execution returns a Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierMessage", "supplierJavaMessage", "supplierKotlinMessage" + }) + public void testMessageSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Message supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify supplier execution returns a Message + Object result = supplier.get(); + assertThat(result).isInstanceOf(Message.class); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierMonoMessage", "supplierJavaMonoMessage", "supplierKotlinMonoMessage" + }) + public void testMonoMessageSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Mono message supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + assertThat(outputTypeName.contains("Mono")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify supplier execution returns a Mono + Object result = supplier.get(); + assertThat(result.toString()).contains("Mono"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierSuspendMessage", "supplierKotlinSuspendMessage" + }) + public void testSuspendMessageSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Suspend message supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + // Suspend functions are wrapped in Flux by Spring Cloud Function + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify supplier execution returns a Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierFluxMessage", "supplierJavaFluxMessage", "supplierKotlinFluxMessage" + }) + public void testFluxMessageSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Flux message supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify supplier execution returns a Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierFlowMessage", "supplierJavaFlowMessage", "supplierKotlinFlowMessage" + }) + public void testFlowMessageSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Flow message supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + // Output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify supplier execution returns a Flow/Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + @ParameterizedTest + @ValueSource(strings = { + "supplierSuspendFlowMessage", "supplierKotlinSuspendFlowMessage" + }) + public void testSuspendFlowMessageSuppliers(String supplierName) { + create(new Class[] { KotlinArityApplication.class }); + + FunctionInvocationWrapper supplier = this.catalog.lookup(supplierName); + + // Test should fail if supplier is not found + assertThat(supplier).as("Supplier not found: " + supplierName).isNotNull(); + + // Verify it's a supplier + assertThat(supplier.isSupplier()).isTrue(); + + // Suspend flow message supplier + String outputTypeName = supplier.getOutputType().getTypeName(); + // Output might be Flow or Flux depending on how Spring Cloud Function handles Kotlin types + assertThat(outputTypeName.contains("Flux")).isTrue(); + assertThat(outputTypeName.contains("Message")).isTrue(); + + // Verify supplier execution returns a Flow/Flux + Object result = supplier.get(); + assertThat(result.toString()).contains("Flux"); + } + + private void create(Class[] types, String... props) { + this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run(); + this.catalog = this.context.getBean(FunctionCatalog.class); + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinArityApplication.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinArityApplication.kt new file mode 100644 index 000000000..4c19fe803 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinArityApplication.kt @@ -0,0 +1,12 @@ +package org.springframework.cloud.function.kotlin.arity + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +open class KotlinArityApplication + +fun main(args: Array) { + SpringApplication.run(KotlinArityApplication::class.java, *args) +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinAritySupplierComponent.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinAritySupplierComponent.kt new file mode 100644 index 000000000..cc928eaf2 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinAritySupplierComponent.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Component +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration +import java.util.UUID + +/** + * Examples of implementing suppliers using Kotlin's function type. + * + * ## List of Combinations Implemented: + * --- Coroutine --- + * 1. () -> R -> supplierKotlinPlain + * 2. () -> Flow -> supplierKotlinFlow + * 3. suspend () -> R -> supplierKotlinSuspendPlain + * 4. suspend () -> Flow -> supplierKotlinSuspendFlow + * --- Reactor --- + * 5. () -> Mono -> supplierKotlinMono + * 6. () -> Flux -> supplierKotlinFlux + * --- Message --- + * 7. () -> Message -> supplierKotlinMessage + * 8. () -> Mono> -> supplierKotlinMonoMessage + * 9. suspend () -> Message -> supplierKotlinSuspendMessage + * 10. () -> Flux> -> supplierKotlinFluxMessage + * 11. () -> Flow> -> supplierKotlinFlowMessage + * 12. suspend () -> Flow> -> supplierKotlinSuspendFlowMessage + * + * @author Adrien Poupard + */ +class KotlinSupplierKotlinExamples + +/** 1) () -> R */ +@Component +class SupplierKotlinPlain : () -> Int { + override fun invoke(): Int { + return 42 + } +} + +/** 2) () -> Flow */ +@Component +class SupplierKotlinFlow : () -> Flow { + override fun invoke(): Flow { + return flow { + emit("A") + emit("B") + emit("C") + } + } +} + +/** 3) suspend () -> R */ +@Component +class SupplierKotlinSuspendPlain : suspend () -> String { + override suspend fun invoke(): String { + return "Hello from suspend" + } +} + +/** 4) suspend () -> Flow */ +@Component +class SupplierKotlinSuspendFlow : suspend () -> Flow { + override suspend fun invoke(): Flow { + return flow { + emit("x") + emit("y") + emit("z") + } + } +} + +/** 5) () -> Mono */ +@Component +class SupplierKotlinMono : () -> Mono { + override fun invoke(): Mono { + return Mono.just("Hello from Mono").delayElement(Duration.ofMillis(50)) + } +} + +/** 6) () -> Flux */ +@Component +class SupplierKotlinFlux : () -> Flux { + override fun invoke(): Flux { + return Flux.just("Alpha", "Beta", "Gamma").delayElements(Duration.ofMillis(20)) + } +} + +/** 7) () -> Message */ +@Component +class SupplierKotlinMessage : () -> Message { + override fun invoke(): Message { + return MessageBuilder.withPayload("Hello from Message") + .setHeader("messageId", UUID.randomUUID().toString()) + .build() + } +} + +/** 8) () -> Mono> */ +@Component +class SupplierKotlinMonoMessage : () -> Mono> { + override fun invoke(): Mono> { + return Mono.just( + MessageBuilder.withPayload("Hello from Mono Message") + .setHeader("monoMessageId", UUID.randomUUID().toString()) + .setHeader("source", "mono") + .build() + ).delayElement(Duration.ofMillis(40)) + } +} + +/** 9) suspend () -> Message */ +@Component +class SupplierKotlinSuspendMessage : suspend () -> Message { + override suspend fun invoke(): Message { + return MessageBuilder.withPayload("Hello from Suspend Message") + .setHeader("suspendMessageId", UUID.randomUUID().toString()) + .setHeader("wasSuspended", true) + .build() + } +} + +/** 10) () -> Flux> */ +@Component +class SupplierKotlinFluxMessage : () -> Flux> { + override fun invoke(): Flux> { + return Flux.just("Msg1", "Msg2") + .delayElements(Duration.ofMillis(30)) + .map { payload -> + MessageBuilder.withPayload(payload) + .setHeader("fluxMessageId", UUID.randomUUID().toString()) + .build() + } + } +} + +/** 11) () -> Flow> */ +@Component +class SupplierKotlinFlowMessage : () -> Flow> { + override fun invoke(): Flow> { + return flow { + listOf("FlowMsg1", "FlowMsg2").forEach { payload -> + emit( + MessageBuilder.withPayload(payload) + .setHeader("flowMessageId", UUID.randomUUID().toString()) + .build() + ) + } + } + } +} + +/** 12) suspend () -> Flow> */ +@Component +class SupplierKotlinSuspendFlowMessage : suspend () -> Flow> { + override suspend fun invoke(): Flow> { + return flow { + listOf("SuspendFlowMsg1", "SuspendFlowMsg2").forEach { payload -> + emit( + MessageBuilder.withPayload(payload) + .setHeader("suspendFlowMessageId", UUID.randomUUID().toString()) + .build() + ) + } + } + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityBean.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityBean.kt new file mode 100644 index 000000000..7c486e1c4 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityBean.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.Message +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +/** + * ## List of Combinations Tested (in requested order): + * --- Coroutine --- + * 1. (T) -> Unit -> consumerPlain + * 2. (Flow) -> Unit -> consumerFlow + * 3. suspend (T) -> Unit -> consumerSuspendPlain + * 4. suspend (Flow) -> Unit -> consumerSuspendFlow + * --- Reactor --- + * 5. (T) -> Mono -> consumerMonoInput + * 6. (Mono) -> Mono -> consumerMono + * 7. (Flux) -> Mono -> consumerFlux + * --- Message --- + * 8. (Message) -> Unit -> consumerMessage + * 9. (Mono>) -> Mono -> consumerMonoMessage + * 10. suspend (Message) -> Unit -> consumerSuspendMessage + * 11. (Flux>) -> Unit -> consumerFluxMessage + * 12. (Flow>) -> Unit -> consumerFlowMessage + * 13. suspend (Flow>) -> Unit -> consumerSuspendFlowMessage + * + * @author Adrien Poupard + */ +@Configuration +open class KotlinConsumerArityBean { + + /** 1) (T) -> Unit */ + @Bean + open fun consumerPlain(): (String) -> Unit = { input -> + println("Consumed: $input") + } + + /** 2) (Flow) -> Unit */ + @Bean + open fun consumerFlow(): (Flow) -> Unit = { flowInput -> + println("Received flow: $flowInput (would collect in coroutine)") + } + + /** 3) suspend (T) -> Unit */ + @Bean + open fun consumerSuspendPlain(): suspend (String) -> Unit = { input -> + println("Suspend consumed: $input") + } + + /** 4) suspend (Flow) -> Unit */ + @Bean + open fun consumerSuspendFlow(): suspend (Flow) -> Unit = { flowInput -> + flowInput.collect { item -> + println("Flow item consumed: $item") + } + } + + /** 5) (T) -> Mono */ + @Bean + open fun consumerMonoInput(): (String) -> Mono = { input -> + Mono.fromRunnable { + println("[Reactor] Consumed T: $input") + } + } + + /** 6) (Mono) -> Mono */ + @Bean + open fun consumerMono(): (Mono) -> Mono = { monoInput -> + monoInput.doOnNext { item -> + println("[Reactor] Consumed Mono item: $item") + }.then() + } + + /** 7) (Flux) -> Mono */ + @Bean + open fun consumerFlux(): (Flux) -> Mono = { fluxInput -> + fluxInput.doOnNext { item -> + println("[Reactor] Consumed Flux item: $item") + }.then() + } + + /** 8) (Message) -> Unit */ + @Bean + open fun consumerMessage(): (Message) -> Unit = { message -> + println("[Message] Consumed payload: ${message.payload}, Headers: ${message.headers}") + } + + /** 9) (Mono>) -> Mono */ + @Bean + open fun consumerMonoMessage(): (Mono>) -> Mono = { monoMsgInput -> + monoMsgInput + .doOnNext { message -> + println("[Message][Mono] Consumed payload: ${message.payload}, Header id: ${message.headers.id}") + } + .then() + } + + /** 10) suspend (Message) -> Unit */ + @Bean + open fun consumerSuspendMessage(): suspend (Message) -> Unit = { message -> + println("[Message][Suspend] Consumed payload: ${message.payload}, Header count: ${message.headers.size}") + } + + /** 11) (Flux>) -> Unit */ + @Bean + open fun consumerFluxMessage(): (Flux>) -> Unit = { fluxMsgInput -> + // Explicit subscription needed here because the lambda itself returns Unit + fluxMsgInput.subscribe { message -> + println("[Message] Consumed Flux payload: ${message.payload}, Headers: ${message.headers}") + } + } + + /** 12) (Flow>) -> Unit */ + @Bean + open fun consumerFlowMessage(): (Flow>) -> Unit = { flowMsgInput -> + // Similar to Flux consumer returning Unit, explicit collection might be needed depending on context. + println("[Message] Received Flow: $flowMsgInput (would need explicit collection if signature returns Unit)") + // Example: + // CoroutineScope(Dispatchers.IO).launch { + // flowMsgInput.collect { message -> println(...) } + // } + } + + /** 13) suspend (Flow>) -> Unit */ + @Bean + open fun consumerSuspendFlowMessage(): suspend (Flow>) -> Unit = { flowMsgInput -> + flowMsgInput.collect { message -> + println("[Message] Consumed Suspend Flow payload: ${message.payload}, Headers: ${message.headers}") + } + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityComponent.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityComponent.kt new file mode 100644 index 000000000..502f0bb25 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityComponent.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import org.springframework.messaging.Message +import org.springframework.stereotype.Component +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +/** + * Examples of implementing consumers using Kotlin's function type. + * + * ## List of Combinations Implemented: + * --- Coroutine --- + * 1. (T) -> Unit -> consumerKotlinPlain + * 2. (Flow) -> Unit -> consumerKotlinFlow + * 3. suspend (T) -> Unit -> consumerKotlinSuspendPlain + * 4. suspend (Flow) -> Unit -> consumerKotlinSuspendFlow + * --- Reactor --- + * 5. (T) -> Mono -> consumerKotlinMonoInput + * 6. (Mono) -> Mono -> consumerKotlinMono + * 7. (Flux) -> Mono -> consumerKotlinFlux + * --- Message --- + * 8. (Message) -> Unit -> consumerKotlinMessage + * 9. (Mono>) -> Mono -> consumerKotlinMonoMessage + * 10. suspend (Message) -> Unit -> consumerKotlinSuspendMessage + * 11. (Flux>) -> Unit -> consumerKotlinFluxMessage + * 12. (Flow>) -> Unit -> consumerKotlinFlowMessage + * 13. suspend (Flow>) -> Unit -> consumerKotlinSuspendFlowMessage + * + * @author Adrien Poupard + */ +class KotlinConsumeKotlinExamples + + +/** 1) (T) -> Unit */ +@Component +class ConsumerKotlinPlain : (String) -> Unit { + override fun invoke(input: String) { + println("Consumed: $input") + } +} + +/** 2) (Flow) -> Unit */ +@Component +class ConsumerKotlinFlow : (Flow) -> Unit { + override fun invoke(flowInput: Flow) { + println("Received flow: $flowInput (would collect in coroutine)") + } +} + +/** 3) suspend (T) -> Unit */ +@Component +class ConsumerKotlinSuspendPlain : suspend (String) -> Unit { + override suspend fun invoke(input: String) { + println("Suspend consumed: $input") + } +} + +/** 4) suspend (Flow) -> Unit */ +@Component +class ConsumerKotlinSuspendFlow : suspend (Flow) -> Unit { + override suspend fun invoke(flowInput: Flow) { + flowInput.collect { item -> + println("Flow item consumed: $item") + } + } +} + +/** 5) (T) -> Mono */ +@Component +class ConsumerKotlinMonoInput : (String) -> Mono { + override fun invoke(input: String): Mono { + return Mono.fromRunnable { + println("[Reactor] Consumed T: $input") + } + } +} + +/** 6) (Mono) -> Mono */ +@Component +class ConsumerKotlinMono : (Mono) -> Mono { + override fun invoke(monoInput: Mono): Mono { + return monoInput.doOnNext { item -> + println("[Reactor] Consumed Mono item: $item") + }.then() + } +} + +/** 7) (Flux) -> Mono */ +@Component +class ConsumerKotlinFlux : (Flux) -> Mono { + override fun invoke(fluxInput: Flux): Mono { + return fluxInput.doOnNext { item -> + println("[Reactor] Consumed Flux item: $item") + }.then() + } +} + +/** 8) (Message) -> Unit */ +@Component +class ConsumerKotlinMessage : (Message) -> Unit { + override fun invoke(message: Message) { + println("[Message] Consumed payload: ${message.payload}, Headers: ${message.headers}") + } +} + +/** 9) (Mono>) -> Mono */ +@Component +class ConsumerKotlinMonoMessage : (Mono>) -> Mono { + override fun invoke(monoMsgInput: Mono>): Mono { + return monoMsgInput + .doOnNext { message -> + println("[Message][Mono] Consumed payload: ${message.payload}, Header id: ${message.headers.id}") + } + .then() + } +} + +/** 10) suspend (Message) -> Unit */ +@Component +class ConsumerKotlinSuspendMessage : suspend (Message) -> Unit { + override suspend fun invoke(message: Message) { + println("[Message][Suspend] Consumed payload: ${message.payload}, Header count: ${message.headers.size}") + } +} + +/** 11) (Flux>) -> Unit */ +@Component +class ConsumerKotlinFluxMessage : (Flux>) -> Unit { + override fun invoke(fluxMsgInput: Flux>) { + // Explicit subscription needed here because the lambda itself returns Unit + fluxMsgInput.subscribe { message -> + println("[Message] Consumed Flux payload: ${message.payload}, Headers: ${message.headers}") + } + } +} + +/** 12) (Flow>) -> Unit */ +@Component +class ConsumerKotlinFlowMessage : (Flow>) -> Unit { + override fun invoke(flowMsgInput: Flow>) { + // Similar to Flux consumer returning Unit, explicit collection might be needed depending on context. + println("[Message] Received Flow: $flowMsgInput (would need explicit collection)") + } +} + +/** 13) suspend (Flow>) -> Unit */ +@Component +class ConsumerKotlinSuspendFlowMessage : suspend (Flow>) -> Unit { + override suspend fun invoke(flowMsgInput: Flow>) { + flowMsgInput.collect { message -> + println("[Message] Consumed Suspend Flow payload: ${message.payload}, Headers: ${message.headers}") + } + } +} + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityJava.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityJava.kt new file mode 100644 index 000000000..3b025d845 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityJava.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import java.util.function.Consumer +import kotlinx.coroutines.flow.Flow +import org.springframework.messaging.Message +import org.springframework.stereotype.Component +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +/** + * Examples of implementing consumers using Java's Consumer interface in Kotlin. + * + * ## List of Combinations Implemented: + * --- Coroutine --- + * 1. Consumer -> consumerJavaPlain + * 2. Consumer> -> consumerJavaFlow + * 3. Consumer with suspend -> consumerJavaSuspendPlain + * 4. Consumer> with suspend -> consumerJavaSuspendFlow + * --- Reactor --- + * 5. Consumer returning Mono -> consumerJavaMonoInput + * 6. Consumer> -> consumerJavaMono + * 7. Consumer> -> consumerJavaFlux + * --- Message --- + * 8. Consumer> -> consumerJavaMessage + * 9. Consumer>> -> consumerJavaMonoMessage + * 10. Consumer> with suspend -> consumerJavaSuspendMessage + * 11. Consumer>> -> consumerJavaFluxMessage + * 12. Consumer>> -> consumerJavaFlowMessage + * 13. Consumer>> with suspend -> consumerJavaSuspendFlowMessage + * + * @author Adrien Poupard + */ +class KotlinConsumerJavaExamples + +/** 1) Consumer */ +@Component +class ConsumerJavaPlain : Consumer { + override fun accept(input: String) { + println("Consumed: $input") + } +} + +/** 2) Consumer> */ +@Component +class ConsumerJavaFlow : Consumer> { + override fun accept(flowInput: Flow) { + println("Received flow: $flowInput (would collect in coroutine)") + } +} + + + +/** 5) Consumer returning Mono */ +@Component +class ConsumerJavaMonoInput : Consumer { + override fun accept(input: String) { + println("[Reactor] Consumed T: $input") + // Note: Consumer doesn't return anything, but we're simulating the Mono behavior + } +} + +/** 6) Consumer> */ +@Component +class ConsumerJavaMono : Consumer> { + override fun accept(monoInput: Mono) { + monoInput.subscribe { item -> + println("[Reactor] Consumed Mono item: $item") + } + } +} + +/** 7) Consumer> */ +@Component +class ConsumerJavaFlux : Consumer> { + override fun accept(fluxInput: Flux) { + fluxInput.subscribe { item -> + println("[Reactor] Consumed Flux item: $item") + } + } +} + +/** 8) Consumer> */ +@Component +class ConsumerJavaMessage : Consumer> { + override fun accept(message: Message) { + println("[Message] Consumed payload: ${message.payload}, Headers: ${message.headers}") + } +} + +/** 9) Consumer>> */ +@Component +class ConsumerJavaMonoMessage : Consumer>> { + override fun accept(monoMsgInput: Mono>) { + monoMsgInput.subscribe { message -> + println("[Message][Mono] Consumed payload: ${message.payload}, Header id: ${message.headers.id}") + } + } +} + + +/** 11) Consumer>> */ +@Component +class ConsumerJavaFluxMessage : Consumer>> { + override fun accept(fluxMsgInput: Flux>) { + fluxMsgInput.subscribe { message -> + println("[Message] Consumed Flux payload: ${message.payload}, Headers: ${message.headers}") + } + } +} + +/** 12) Consumer>> */ +@Component +class ConsumerJavaFlowMessage : Consumer>> { + override fun accept(flowMsgInput: Flow>) { + println("[Message] Received Flow: $flowMsgInput (would need explicit collection)") + } +} + +//} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityBean.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityBean.kt new file mode 100644 index 000000000..feb5958bb --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityBean.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration + +/** + * ## List of Combinations Tested (in requested order): + * --- Coroutine --- + * 1. (T) -> R -> functionPlainToPlain + * 2. (T) -> Flow -> functionPlainToFlow + * 3. (Flow) -> R -> functionFlowToPlain + * 4. (Flow) -> Flow -> functionFlowToFlow + * 5. suspend (T) -> R -> functionSuspendPlainToPlain + * 6. suspend (T) -> Flow -> functionSuspendPlainToFlow + * 7. suspend (Flow) -> R -> functionSuspendFlowToPlain + * 8. suspend (Flow) -> Flow -> functionSuspendFlowToFlow + * --- Reactor --- + * 9. (T) -> Mono -> functionPlainToMono + * 10. (T) -> Flux -> functionPlainToFlux + * 11. (Mono) -> Mono -> functionMonoToMono + * 12. (Flux) -> Flux -> functionFluxToFlux + * 13. (Flux) -> Mono -> functionFluxToMono + * --- Message --- + * 14. (Message) -> Message -> functionMessageToMessage + * 15. suspend (Message) -> Message -> functionSuspendMessageToMessage + * 16. (Mono>) -> Mono> -> functionMonoMessageToMonoMessage + * 17. (Flux>) -> Flux> -> functionFluxMessageToFluxMessage + * 18. (Flow>) -> Flow> -> functionFlowMessageToFlowMessage + * 19. suspend (Flow>) -> Flow> -> functionSuspendFlowMessageToFlowMessage + * + * @author Adrien Poupard + */ +@Configuration +open class KotlinFunctionArityBean { + + /** 1) (T) -> R */ + @Bean + open fun functionPlainToPlain(): (String) -> Int = { input -> + input.length + } + + /** 2) (T) -> Flow */ + @Bean + open fun functionPlainToFlow(): (String) -> Flow = { input -> + flow { + input.forEach { c -> emit(c.toString()) } + } + } + + /** 3) (Flow) -> R */ + @Bean + open fun functionFlowToPlain(): (Flow) -> Int = { flowInput -> + var count = 0 + runBlocking { + flowInput.collect { count++ } + } + count + } + + /** 4) (Flow) -> Flow */ + @Bean + open fun functionFlowToFlow(): (Flow) -> Flow = { flowInput -> + flowInput.map { it.toString() } + } + + /** 5) suspend (T) -> R */ + @Bean + open fun functionSuspendPlainToPlain(): suspend (String) -> Int = { input -> + input.length + } + + /** 6) suspend (T) -> Flow */ + @Bean + open fun functionSuspendPlainToFlow(): suspend (String) -> Flow = { input -> + flow { + input.forEach { c -> emit(c.toString()) } + } + } + + /** 7) suspend (Flow) -> R */ + @Bean + open fun functionSuspendFlowToPlain(): suspend (Flow) -> Int = { flowInput -> + var count = 0 + flowInput.collect { count++ } + count + } + + /** 8) suspend (Flow) -> Flow */ + @Bean + open fun functionSuspendFlowToFlow(): suspend (Flow) -> Flow = { incomingFlow -> + flow { + incomingFlow.collect { item -> emit(item.uppercase()) } + } + } + + /** 9) (T) -> Mono */ + @Bean + open fun functionPlainToMono(): (String) -> Mono = { input -> + Mono.just(input.length).delayElement(Duration.ofMillis(50)) + } + + /** 10) (T) -> Flux */ + @Bean + open fun functionPlainToFlux(): (String) -> Flux = { input -> + Flux.fromIterable(input.toList()).map { it.toString() } + } + + /** 11) (Mono) -> Mono */ + @Bean + open fun functionMonoToMono(): (Mono) -> Mono = { monoInput -> + monoInput.map { it.uppercase() }.delayElement(Duration.ofMillis(50)) + } + + /** 12) (Flux) -> Flux */ + @Bean + open fun functionFluxToFlux(): (Flux) -> Flux = { fluxInput -> + fluxInput.map { it.length } + } + + /** 13) (Flux) -> Mono */ + @Bean + open fun functionFluxToMono(): (Flux) -> Mono = { fluxInput -> + fluxInput.count().map { it.toInt() } + } + + /** 14) (Message) -> Message */ + @Bean + open fun functionMessageToMessage(): (Message) -> Message = { message -> + MessageBuilder.withPayload(message.payload.length) + .copyHeaders(message.headers) + .setHeader("processed", "true") + .build() + } + + /** 15) suspend (Message) -> Message */ + @Bean + open fun functionSuspendMessageToMessage(): suspend (Message) -> Message = { message -> + MessageBuilder.withPayload(message.payload.length * 2) + .copyHeaders(message.headers) + .setHeader("suspend-processed", "true") + .setHeader("original-id", message.headers["original-id"] ?: "N/A") + .build() + } + + /** 16) (Mono>) -> Mono> */ + @Bean + open fun functionMonoMessageToMonoMessage(): (Mono>) -> Mono> = { monoMsgInput -> + monoMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.hashCode()) + .copyHeaders(message.headers) + .setHeader("mono-processed", "true") + .build() + } + } + + /** 17) (Flux>) -> Flux> */ + @Bean + open fun functionFluxMessageToFluxMessage(): (Flux>) -> Flux> = { fluxMsgInput -> + fluxMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.uppercase()) + .copyHeaders(message.headers) + .setHeader("flux-processed", "true") + .build() + } + } + + /** 18) (Flow>) -> Flow> */ + @Bean + open fun functionFlowMessageToFlowMessage(): (Flow>) -> Flow> = { flowMsgInput -> + flowMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.reversed()) + .copyHeaders(message.headers) + .setHeader("flow-processed", "true") + .build() + } + } + + /** 19) suspend (Flow>) -> Flow> */ + @Bean + open fun functionSuspendFlowMessageToFlowMessage(): suspend (Flow>) -> Flow> = { flowMsgInput -> + flow { + flowMsgInput.collect { message -> + emit( + MessageBuilder.withPayload(message.payload.plus(" SUSPEND")) + .copyHeaders(message.headers) + .setHeader("suspend-flow-processed", "true") + .build() + ) + } + } + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityComponent.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityComponent.kt new file mode 100644 index 000000000..811f683f0 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityComponent.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Component +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration + +/** + * Examples of implementing functions using Kotlin's function type. + * + * ## List of Combinations Implemented: + * --- Coroutine --- + * 1. (T) -> R -> functionKotlinPlainToPlain + * 2. (T) -> Flow -> functionKotlinPlainToFlow + * 3. (Flow) -> R -> functionKotlinFlowToPlain + * 4. (Flow) -> Flow -> functionKotlinFlowToFlow + * 5. suspend (T) -> R -> functionKotlinSuspendPlainToPlain + * 6. suspend (T) -> Flow -> functionKotlinSuspendPlainToFlow + * 7. suspend (Flow) -> R -> functionKotlinSuspendFlowToPlain + * 8. suspend (Flow) -> Flow -> functionKotlinSuspendFlowToFlow + * --- Reactor --- + * 9. (T) -> Mono -> functionKotlinPlainToMono + * 10. (T) -> Flux -> functionKotlinPlainToFlux + * 11. (Mono) -> Mono -> functionKotlinMonoToMono + * 12. (Flux) -> Flux -> functionKotlinFluxToFlux + * 13. (Flux) -> Mono -> functionKotlinFluxToMono + * --- Message --- + * 14. (Message) -> Message -> functionKotlinMessageToMessage + * 15. suspend (Message) -> Message -> functionKotlinSuspendMessageToMessage + * 16. (Mono>) -> Mono> -> functionKotlinMonoMessageToMonoMessage + * 17. (Flux>) -> Flux> -> functionKotlinFluxMessageToFluxMessage + * 18. (Flow>) -> Flow> -> functionKotlinFlowMessageToFlowMessage + * 19. suspend (Flow>) -> Flow> -> functionKotlinSuspendFlowMessageToFlowMessage + * + * @author Adrien Poupard + */ +class KotlinFunctionKotlinExamples + +/** 1) (T) -> R */ +@Component +open class FunctionKotlinPlainToPlain : (String) -> Int { + override fun invoke(input: String): Int { + return input.length + } +} + +/** 2) (T) -> Flow */ +@Component +class FunctionKotlinPlainToFlow : (String) -> Flow { + override fun invoke(input: String): Flow { + return flow { + input.forEach { c -> emit(c.toString()) } + } + } +} + +/** 3) (Flow) -> R */ +@Component +class FunctionKotlinFlowToPlain : (Flow) -> Int { + override fun invoke(flowInput: Flow): Int { + var count = 0 + runBlocking { + flowInput.collect { count++ } + } + return count + } +} + +/** 4) (Flow) -> Flow */ +@Component +class FunctionKotlinFlowToFlow : (Flow) -> Flow { + override fun invoke(flowInput: Flow): Flow { + return flowInput.map { it.toString() } + } +} + +/** 5) suspend (T) -> R */ +@Component +class FunctionKotlinSuspendPlainToPlain : suspend (String) -> Int { + override suspend fun invoke(input: String): Int { + return input.length + } +} + +/** 6) suspend (T) -> Flow */ +@Component +class FunctionKotlinSuspendPlainToFlow : suspend (String) -> Flow { + override suspend fun invoke(input: String): Flow { + return flow { + input.forEach { c -> emit(c.toString()) } + } + } +} + +/** 7) suspend (Flow) -> R */ +@Component +class FunctionKotlinSuspendFlowToPlain : suspend (Flow) -> Int { + override suspend fun invoke(flowInput: Flow): Int { + var count = 0 + flowInput.collect { count++ } + return count + } +} + +/** 8) suspend (Flow) -> Flow */ +@Component +class FunctionKotlinSuspendFlowToFlow : suspend (Flow) -> Flow { + override suspend fun invoke(incomingFlow: Flow): Flow { + return flow { + incomingFlow.collect { item -> emit(item.uppercase()) } + } + } +} + +/** 9) (T) -> Mono */ +@Component +class FunctionKotlinPlainToMono : (String) -> Mono { + override fun invoke(input: String): Mono { + return Mono.just(input.length).delayElement(Duration.ofMillis(50)) + } +} + +/** 10) (T) -> Flux */ +@Component +class FunctionKotlinPlainToFlux : (String) -> Flux { + override fun invoke(input: String): Flux { + return Flux.fromIterable(input.toList()).map { it.toString() } + } +} + +/** 11) (Mono) -> Mono */ +@Component +class FunctionKotlinMonoToMono : (Mono) -> Mono { + override fun invoke(monoInput: Mono): Mono { + return monoInput.map { it.uppercase() }.delayElement(Duration.ofMillis(50)) + } +} + +/** 12) (Flux) -> Flux */ +@Component +class FunctionKotlinFluxToFlux : (Flux) -> Flux { + override fun invoke(fluxInput: Flux): Flux { + return fluxInput.map { it.length } + } +} + +/** 13) (Flux) -> Mono */ +@Component +class FunctionKotlinFluxToMono : (Flux) -> Mono { + override fun invoke(fluxInput: Flux): Mono { + return fluxInput.count().map { it.toInt() } + } +} + +/** 14) (Message) -> Message */ +@Component +class FunctionKotlinMessageToMessage : (Message) -> Message { + override fun invoke(message: Message): Message { + return MessageBuilder.withPayload(message.payload.length) + .copyHeaders(message.headers) + .setHeader("processed", "true") + .build() + } +} + +/** 15) suspend (Message) -> Message */ +@Component +class FunctionKotlinSuspendMessageToMessage : suspend (Message) -> Message { + override suspend fun invoke(message: Message): Message { + return MessageBuilder.withPayload(message.payload.length * 2) + .copyHeaders(message.headers) + .setHeader("suspend-processed", "true") + .setHeader("original-id", message.headers["original-id"] ?: "N/A") + .build() + } +} + +/** 16) (Mono>) -> Mono> */ +@Component +class FunctionKotlinMonoMessageToMonoMessage : (Mono>) -> Mono> { + override fun invoke(monoMsgInput: Mono>): Mono> { + return monoMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.hashCode()) + .copyHeaders(message.headers) + .setHeader("mono-processed", "true") + .build() + } + } +} + +/** 17) (Flux>) -> Flux> */ +@Component +class FunctionKotlinFluxMessageToFluxMessage : (Flux>) -> Flux> { + override fun invoke(fluxMsgInput: Flux>): Flux> { + return fluxMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.uppercase()) + .copyHeaders(message.headers) + .setHeader("flux-processed", "true") + .build() + } + } +} + +/** 18) (Flow>) -> Flow> */ +@Component +class FunctionKotlinFlowMessageToFlowMessage : (Flow>) -> Flow> { + override fun invoke(flowMsgInput: Flow>): Flow> { + return flowMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.reversed()) + .copyHeaders(message.headers) + .setHeader("flow-processed", "true") + .build() + } + } +} + +/** 19) suspend (Flow>) -> Flow> */ +@Component +class FunctionKotlinSuspendFlowMessageToFlowMessage : suspend (Flow>) -> Flow> { + override suspend fun invoke(flowMsgInput: Flow>): Flow> { + return flow { + flowMsgInput.collect { message -> + emit( + MessageBuilder.withPayload(message.payload.plus(" SUSPEND")) + .copyHeaders(message.headers) + .setHeader("suspend-flow-processed", "true") + .build() + ) + } + } + } +} + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityJava.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityJava.kt new file mode 100644 index 000000000..9155b8554 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityJava.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import java.util.function.Function +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Component +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration + +/** + * Examples of implementing functions using Java's Function interface in Kotlin. + * + * ## List of Combinations Implemented: + * --- Coroutine --- + * 1. Function -> functionJavaPlainToPlain + * 2. Function> -> functionJavaPlainToFlow + * 3. Function, R> -> functionJavaFlowToPlain + * 4. Function, Flow> -> functionJavaFlowToFlow + * 5. Function with suspend -> functionJavaSuspendPlainToPlain + * 6. Function> with suspend -> functionJavaSuspendPlainToFlow + * 7. Function, R> with suspend -> functionJavaSuspendFlowToPlain + * 8. Function, Flow> with suspend -> functionJavaSuspendFlowToFlow + * --- Reactor --- + * 9. Function> -> functionJavaPlainToMono + * 10. Function> -> functionJavaPlainToFlux + * 11. Function, Mono> -> functionJavaMonoToMono + * 12. Function, Flux> -> functionJavaFluxToFlux + * 13. Function, Mono> -> functionJavaFluxToMono + * --- Message --- + * 14. Function, Message> -> functionJavaMessageToMessage + * 15. Function, Message> with suspend -> functionJavaSuspendMessageToMessage + * 16. Function>, Mono>> -> functionJavaMonoMessageToMonoMessage + * 17. Function>, Flux>> -> functionJavaFluxMessageToFluxMessage + * 18. Function>, Flow>> -> functionJavaFlowMessageToFlowMessage + * 19. Function>, Flow>> with suspend -> functionJavaSuspendFlowMessageToFlowMessage + * + * @author Adrien Poupard + */ +class KotlinFunctionJavaExamples + +/** 1) Function */ +@Component +class FunctionJavaPlainToPlain : Function { + override fun apply(input: String): Int { + return input.length + } +} + +/** 2) Function> */ +@Component +class FunctionJavaPlainToFlow : Function> { + override fun apply(input: String): Flow { + return flow { + input.forEach { c -> emit(c.toString()) } + } + } +} + +/** 3) Function, R> */ +@Component +class FunctionJavaFlowToPlain : Function, Int> { + override fun apply(flowInput: Flow): Int { + var count = 0 + runBlocking { + flowInput.collect { count++ } + } + return count + } +} + +/** 4) Function, Flow> */ +@Component +class FunctionJavaFlowToFlow : Function, Flow> { + override fun apply(flowInput: Flow): Flow { + return flowInput.map { it.toString() } + } +} + + + + + +/** 9) Function> */ +@Component +class FunctionJavaPlainToMono : Function> { + override fun apply(input: String): Mono { + return Mono.just(input.length).delayElement(Duration.ofMillis(50)) + } +} + +/** 10) Function> */ +@Component +class FunctionJavaPlainToFlux : Function> { + override fun apply(input: String): Flux { + return Flux.fromIterable(input.toList()).map { it.toString() } + } +} + +/** 11) Function, Mono> */ +@Component +class FunctionJavaMonoToMono : Function, Mono> { + override fun apply(monoInput: Mono): Mono { + return monoInput.map { it.uppercase() }.delayElement(Duration.ofMillis(50)) + } +} + +/** 12) Function, Flux> */ +@Component +class FunctionJavaFluxToFlux : Function, Flux> { + override fun apply(fluxInput: Flux): Flux { + return fluxInput.map { it.length } + } +} + +/** 13) Function, Mono> */ +@Component +class FunctionJavaFluxToMono : Function, Mono> { + override fun apply(fluxInput: Flux): Mono { + return fluxInput.count().map { it.toInt() } + } +} + +/** 14) Function, Message> */ +@Component +class FunctionJavaMessageToMessage : Function, Message> { + override fun apply(message: Message): Message { + return MessageBuilder.withPayload(message.payload.length) + .copyHeaders(message.headers) + .setHeader("processed", "true") + .build() + } +} + + +/** 16) Function>, Mono>> */ +@Component +class FunctionJavaMonoMessageToMonoMessage : Function>, Mono>> { + override fun apply(monoMsgInput: Mono>): Mono> { + return monoMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.hashCode()) + .copyHeaders(message.headers) + .setHeader("mono-processed", "true") + .build() + } + } +} + +/** 17) Function>, Flux>> */ +@Component +class FunctionJavaFluxMessageToFluxMessage : Function>, Flux>> { + override fun apply(fluxMsgInput: Flux>): Flux> { + return fluxMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.uppercase()) + .copyHeaders(message.headers) + .setHeader("flux-processed", "true") + .build() + } + } +} + +/** 18) Function>, Flow>> */ +@Component +class FunctionJavaFlowMessageToFlowMessage : Function>, Flow>> { + override fun apply(flowMsgInput: Flow>): Flow> { + return flowMsgInput.map { message -> + MessageBuilder.withPayload(message.payload.reversed()) + .copyHeaders(message.headers) + .setHeader("flow-processed", "true") + .build() + } + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityBean.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityBean.kt new file mode 100644 index 000000000..56faa2aca --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityBean.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration +import java.util.UUID + +/** + * ## List of Combinations Tested (in requested order): + * --- Coroutine --- + * 1. () -> R -> supplierPlain + * 2. () -> Flow -> supplierFlow + * 3. suspend () -> R -> supplierSuspendPlain + * 4. suspend () -> Flow -> supplierSuspendFlow + * --- Reactor --- + * 5. () -> Mono -> supplierMono + * 6. () -> Flux -> supplierFlux + * --- Message --- + * 7. () -> Message -> supplierMessage + * 8. () -> Mono> -> supplierMonoMessage + * 9. suspend () -> Message -> supplierSuspendMessage + * 10. () -> Flux> -> supplierFluxMessage + * 11. () -> Flow> -> supplierFlowMessage + * 12. suspend () -> Flow> -> supplierSuspendFlowMessage + * + * @author Adrien Poupard + */ +@Configuration +open class KotlinSupplierArityBean { + + /** 1) () -> R */ + @Bean + open fun supplierPlain(): () -> Int = { + 42 + } + + /** 2) () -> Flow */ + @Bean + open fun supplierFlow(): () -> Flow = { + flow { + emit("A") + emit("B") + emit("C") + } + } + + /** 3) suspend () -> R */ + @Bean + open fun supplierSuspendPlain(): suspend () -> String = { + "Hello from suspend" + } + + /** 4) suspend () -> Flow */ + @Bean + open fun supplierSuspendFlow(): suspend () -> Flow = { + flow { + emit("x") + emit("y") + emit("z") + } + } + + /** 5) () -> Mono */ + @Bean + open fun supplierMono(): () -> Mono = { + Mono.just("Hello from Mono").delayElement(Duration.ofMillis(50)) + } + + /** 6) () -> Flux */ + @Bean + open fun supplierFlux(): () -> Flux = { + Flux.just("Alpha", "Beta", "Gamma").delayElements(Duration.ofMillis(20)) + } + + /** 7) () -> Message */ + @Bean + open fun supplierMessage(): () -> Message = { + MessageBuilder.withPayload("Hello from Message") + .setHeader("messageId", UUID.randomUUID().toString()) + .build() + } + + /** 8) () -> Mono> */ + @Bean + open fun supplierMonoMessage(): () -> Mono> = { + Mono.just( + MessageBuilder.withPayload("Hello from Mono Message") + .setHeader("monoMessageId", UUID.randomUUID().toString()) + .setHeader("source", "mono") + .build() + ).delayElement(Duration.ofMillis(40)) + } + + /** 9) suspend () -> Message */ + @Bean + open fun supplierSuspendMessage(): suspend () -> Message = { + MessageBuilder.withPayload("Hello from Suspend Message") + .setHeader("suspendMessageId", UUID.randomUUID().toString()) + .setHeader("wasSuspended", true) + .build() + } + + /** 10) () -> Flux> */ + @Bean + open fun supplierFluxMessage(): () -> Flux> = { + Flux.just("Msg1", "Msg2") + .delayElements(Duration.ofMillis(30)) + .map { payload -> + MessageBuilder.withPayload(payload) + .setHeader("fluxMessageId", UUID.randomUUID().toString()) + .build() + } + } + + /** 11) () -> Flow> */ + @Bean + open fun supplierFlowMessage(): () -> Flow> = { + flow { + listOf("FlowMsg1", "FlowMsg2").forEach { payload -> + emit( + MessageBuilder.withPayload(payload) + .setHeader("flowMessageId", UUID.randomUUID().toString()) + .build() + ) + } + } + } + + /** 12) suspend () -> Flow> */ + @Bean + open fun supplierSuspendFlowMessage(): suspend () -> Flow> = { + flow { + listOf("SuspendFlowMsg1", "SuspendFlowMsg2").forEach { payload -> + emit( + MessageBuilder.withPayload(payload) + .setHeader("suspendFlowMessageId", UUID.randomUUID().toString()) + .build() + ) + } + } + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityJava.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityJava.kt new file mode 100644 index 000000000..55131254f --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityJava.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.arity + +import java.util.function.Supplier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.springframework.messaging.Message +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Component +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration +import java.util.UUID + +/** + * Examples of implementing suppliers using Java's Supplier interface in Kotlin. + * + * ## List of Combinations Implemented: + * --- Coroutine --- + * 1. Supplier -> supplierJavaPlain + * 2. Supplier> -> supplierJavaFlow + * 3. Supplier with suspend -> supplierJavaSuspendPlain + * 4. Supplier> with suspend -> supplierJavaSuspendFlow + * --- Reactor --- + * 5. Supplier> -> supplierJavaMono + * 6. Supplier> -> supplierJavaFlux + * --- Message --- + * 7. Supplier> -> supplierJavaMessage + * 8. Supplier>> -> supplierJavaMonoMessage + * 9. Supplier> with suspend -> supplierJavaSuspendMessage + * 10. Supplier>> -> supplierJavaFluxMessage + * 11. Supplier>> -> supplierJavaFlowMessage + * 12. Supplier>> with suspend -> supplierJavaSuspendFlowMessage + * + * @author Adrien Poupard + */ +class KotlinSupplierJavaExamples + +/** 1) Supplier */ +@Component +class SupplierJavaPlain : Supplier { + override fun get(): Int { + return 42 + } +} + +/** 2) Supplier> */ +@Component +class SupplierJavaFlow : Supplier> { + override fun get(): Flow { + return flow { + emit("A") + emit("B") + emit("C") + } + } +} + + + +/** 5) Supplier> */ +@Component +class SupplierJavaMono : Supplier> { + override fun get(): Mono { + return Mono.just("Hello from Mono").delayElement(Duration.ofMillis(50)) + } +} + +/** 6) Supplier> */ +@Component +class SupplierJavaFlux : Supplier> { + override fun get(): Flux { + return Flux.just("Alpha", "Beta", "Gamma").delayElements(Duration.ofMillis(20)) + } +} + +/** 7) Supplier> */ +@Component +class SupplierJavaMessage : Supplier> { + override fun get(): Message { + return MessageBuilder.withPayload("Hello from Message") + .setHeader("messageId", UUID.randomUUID().toString()) + .build() + } +} + +/** 8) Supplier>> */ +@Component +class SupplierJavaMonoMessage : Supplier>> { + override fun get(): Mono> { + return Mono.just( + MessageBuilder.withPayload("Hello from Mono Message") + .setHeader("monoMessageId", UUID.randomUUID().toString()) + .setHeader("source", "mono") + .build() + ).delayElement(Duration.ofMillis(40)) + } +} + + +/** 10) Supplier>> */ +@Component +class SupplierJavaFluxMessage : Supplier>> { + override fun get(): Flux> { + return Flux.just("Msg1", "Msg2") + .delayElements(Duration.ofMillis(30)) + .map { payload -> + MessageBuilder.withPayload(payload) + .setHeader("fluxMessageId", UUID.randomUUID().toString()) + .build() + } + } +} + +/** 11) Supplier>> */ +@Component +class SupplierJavaFlowMessage : Supplier>> { + override fun get(): Flow> { + return flow { + listOf("FlowMsg1", "FlowMsg2").forEach { payload -> + emit( + MessageBuilder.withPayload(payload) + .setHeader("flowMessageId", UUID.randomUUID().toString()) + .build() + ) + } + } + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinConsumerArityBeanTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinConsumerArityBeanTest.kt new file mode 100644 index 000000000..09e8de0cd --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinConsumerArityBeanTest.kt @@ -0,0 +1,383 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.web + +import java.time.Duration +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.function.context.test.FunctionalSpringBootTest +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication +import org.springframework.cloud.function.kotlin.arity.KotlinConsumerArityBean +import org.springframework.http.MediaType +import org.springframework.test.web.reactive.server.WebTestClient + +/** + * Test class for verifying the Kotlin Consumer examples in [KotlinConsumerArityBean]. + * Each bean is exposed at "/{beanName}" by Spring Cloud Function. + * + * ## Consumers Tested: + * --- Coroutine --- + * 1. (T) -> Unit -> consumerPlain + * 2. (Flow) -> Unit -> consumerFlow + * 3. suspend (T) -> Unit -> consumerSuspendPlain + * 4. suspend (Flow) -> Unit -> consumerSuspendFlow + * --- Reactor --- + * 5. (T) -> Mono -> consumerMonoInput + * 6. (Mono) -> Mono -> consumerMono + * 7. (Flux) -> Mono -> consumerFlux + * --- Message --- + * 8. (Message) -> Unit -> consumerMessage + * 9. (Mono>) -> Mono -> consumerMonoMessage + * 10. suspend (Message) -> Unit -> consumerSuspendMessage + * 11. (Flux>) -> Unit -> consumerFluxMessage + * 12. (Flow>) -> Unit -> consumerFlowMessage + * 13. suspend (Flow>) -> Unit -> consumerSuspendFlowMessage + * + * @author Adrien Poupard + */ +@FunctionalSpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = [KotlinArityApplication::class] +) +@AutoConfigureWebTestClient +class KotlinConsumerArityBeanTest { + + @Autowired + lateinit var webTestClient: WebTestClient + + @BeforeEach + fun setup() { + this.webTestClient = webTestClient.mutate() + .responseTimeout(Duration.ofSeconds(120)) + .build() + } + + /** + * 1. (T) -> Unit -> consumerPlain, consumerJavaPlain + * Takes a String (side-effect only). + * + * --- Input: --- + * POST /consumerPlain + * "Log me" + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerPlain", "consumerJavaPlain", "consumerKotlinPlain"]) + fun testConsumerPlain(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("Log me") + .exchange() + .expectStatus().isAccepted + } + + /** + * 2. (Flow) -> Unit -> consumerFlow, consumerJavaFlow + * Takes a Flow of Strings (side-effect only). + * + * --- Input: --- + * POST /consumerFlow + * Content-Type: application/json + * ["one","two"] + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerFlow", "consumerJavaFlow", "consumerKotlinFlow"]) + fun testConsumerFlow(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("one", "two")) + .exchange() + .expectStatus().isAccepted + } + + /** + * 3. suspend (T) -> Unit -> consumerSuspendPlain + * Suspending consumer that takes a String (side-effect only). + * + * --- Input: --- + * POST /consumerSuspendPlain + * "test" + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerSuspendPlain", "consumerKotlinSuspendPlain"]) + fun testConsumerSuspendPlain(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("test") + .exchange() + .expectStatus().isAccepted + } + + /** + * 4. suspend (Flow) -> Unit -> consumerSuspendFlow + * Suspending consumer that takes a Flow of Strings (side-effect only). + * + * --- Input: --- + * POST /consumerSuspendFlow + * Content-Type: application/json + * ["foo","bar"] + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerSuspendFlow", "consumerKotlinSuspendFlow"]) + fun testConsumerSuspendFlow(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("foo", "bar")) + .exchange() + .expectStatus().isAccepted + } + + + /** + * 5. (T) -> Mono -> consumerMonoInput, consumerJavaMonoInput + * Consumer takes String input, returns Mono. + * + * --- Input: --- + * POST /consumerMonoInput + * "Consume Me (Mono Input)" + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerMonoInput", "consumerJavaMonoInput", "consumerKotlinMonoInput"]) + fun testConsumerMonoInput(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("Consume Me (Mono Input)") + .exchange() + .expectStatus().isAccepted + } + + /** + * 6. (Mono) -> Mono -> consumerMono, consumerJavaMono + * Consumer takes Mono, returns Mono. + * + * --- Input: --- + * POST /consumerMono + * "Consume Me (Mono)" + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerMono", "consumerJavaMono", "consumerKotlinMono"]) + fun testConsumerMono(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("Consume Me (Mono)") + .exchange() + .expectStatus().isAccepted + } + + /** + * 7. (Flux) -> Mono -> consumerFlux, consumerJavaFlux + * Consumer takes Flux, returns Mono. + * + * --- Input: --- + * POST /consumerFlux + * Content-Type: application/json + * ["Consume","Flux","Items"] + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerFlux", "consumerJavaFlux", "consumerKotlinFlux"]) + fun testConsumerFlux(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("Consume", "Flux", "Items")) + .exchange() + .expectStatus().isAccepted + } + + /** + * 8. (Message) -> Unit -> consumerMessage, consumerJavaMessage + * Consumer takes Message (side-effect only). + * + * --- Input: --- + * POST /consumerMessage + * Header: inputHeader=inValue + * "Consume Me (Message)" + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerMessage", "consumerJavaMessage", "consumerKotlinMessage"]) + fun testConsumerMessage(name: String) { + webTestClient.post() + .uri("/$name") + .header("inputHeader", "inValue") + .bodyValue("Consume Me (Message)") + .exchange() + .expectStatus().isAccepted + } + + /** + * 9. (Mono>) -> Mono -> consumerMonoMessage, consumerJavaMonoMessage + * Consumer takes Mono>, returns Mono. + * + * --- Input: --- + * POST /consumerMonoMessage + * Header: monoMsgHeader=monoMsgValue + * "Consume Me (Mono Message)" + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerMonoMessage", "consumerJavaMonoMessage", "consumerKotlinMonoMessage"]) + fun testConsumerMonoMessage(name: String) { + webTestClient.post() + .uri("/$name") + .header("monoMsgHeader", "monoMsgValue") + .bodyValue("Consume Me (Mono Message)") + .exchange() + .expectStatus().isAccepted + } + + /** + * 10. suspend (Message) -> Unit -> consumerSuspendMessage + * Suspending consumer takes Message. + * + * --- Input: --- + * POST /consumerSuspendMessage + * Header: suspendInputHeader=susInValue + * "Consume Me (Suspend Message)" + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerSuspendMessage", "consumerKotlinSuspendMessage"]) + fun testConsumerSuspendMessage(name: String) { + webTestClient.post() + .uri("/$name") + .header("suspendInputHeader", "susInValue") + .bodyValue("Consume Me (Suspend Message)") + .exchange() + .expectStatus().isAccepted + } + + /** + * 11. (Flux>) -> Unit -> consumerFluxMessage, consumerJavaFluxMessage + * Consumer takes Flux>. + * + * --- Input: --- + * POST /consumerFluxMessage + * Content-Type: application/json + * Header: fluxInputHeader=fluxInValue + * ["Consume", "Flux", "Messages"] + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerFluxMessage", "consumerJavaFluxMessage", "consumerKotlinFluxMessage"]) + fun testConsumerFluxMessage(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .header("fluxInputHeader", "fluxInValue") + .bodyValue(listOf("Consume", "Flux", "Messages")) + .exchange() + .expectStatus().isAccepted + } + + /** + * 12. (Flow>) -> Unit -> consumerFlowMessage, consumerJavaFlowMessage + * Consumer takes Flow>. + * + * --- Input: --- + * POST /consumerFlowMessage + * Content-Type: application/json + * Header: flowInputHeader=flowInValue + * ["Consume", "Flow", "Messages"] + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerFlowMessage", "consumerJavaFlowMessage", "consumerKotlinFlowMessage"]) + fun testConsumerFlowMessage(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .header("flowInputHeader", "flowInValue") + .bodyValue(listOf("Consume", "Flow", "Messages")) + .exchange() + .expectStatus().isAccepted + } + + /** + * 13. suspend (Flow>) -> Unit -> consumerSuspendFlowMessage + * Suspending consumer takes Flow>. + * + * --- Input: --- + * POST /consumerSuspendFlowMessage + * Content-Type: application/json + * Header: suspendFlowInputHeader=suspendFlowInValue + * ["Consume", "Suspend Flow", "Messages"] + * + * --- Output: --- + * 202 ACCEPTED + * (No body) + */ + @ParameterizedTest + @ValueSource(strings = ["consumerSuspendFlowMessage", "consumerKotlinSuspendFlowMessage"]) + fun testConsumerSuspendFlowMessage(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .header("suspendFlowInputHeader", "suspendFlowInValue") + .bodyValue(listOf("Consume", "Suspend Flow", "Messages")) + .exchange() + .expectStatus().isAccepted + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinFunctionArityBeanTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinFunctionArityBeanTest.kt new file mode 100644 index 000000000..b4672b24f --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinFunctionArityBeanTest.kt @@ -0,0 +1,592 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.web + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.function.context.test.FunctionalSpringBootTest +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication +import org.springframework.cloud.function.kotlin.arity.KotlinFunctionArityBean +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.MediaType +import org.springframework.test.web.reactive.server.WebTestClient +import java.time.Duration +import java.util.UUID + +/** + * Test class for verifying the Kotlin Function examples in [KotlinFunctionArityBean], [KotlinFunctionJavaExamples], and [KotlinFunctionKotlinExamples]. + * Each bean is exposed at "/{beanName}" by Spring Cloud Function. + * + * ## Functions Tested: + * --- Coroutine --- + * 1. (T) -> R -> functionPlainToPlain, functionJavaPlainToPlain, functionKotlinPlainToPlain + * 2. (T) -> Flow -> functionPlainToFlow, functionJavaPlainToFlow, functionKotlinPlainToFlow + * 3. (Flow) -> R -> functionFlowToPlain, functionJavaFlowToPlain, functionKotlinFlowToPlain + * 4. (Flow) -> Flow -> functionFlowToFlow, functionJavaFlowToFlow, functionKotlinFlowToFlow + * 5. suspend (T) -> R -> functionSuspendPlainToPlain, functionJavaSuspendPlainToPlain, functionKotlinSuspendPlainToPlain + * 6. suspend (T) -> Flow -> functionSuspendPlainToFlow, functionJavaSuspendPlainToFlow, functionKotlinSuspendPlainToFlow + * 7. suspend (Flow) -> R -> functionSuspendFlowToPlain, functionJavaSuspendFlowToPlain, functionKotlinSuspendFlowToPlain + * 8. suspend (Flow) -> Flow -> functionSuspendFlowToFlow, functionJavaSuspendFlowToFlow, functionKotlinSuspendFlowToFlow + * --- Reactor --- + * 9. (T) -> Mono -> functionPlainToMono, functionJavaPlainToMono, functionKotlinPlainToMono + * 10. (T) -> Flux -> functionPlainToFlux, functionJavaPlainToFlux, functionKotlinPlainToFlux + * 11. (Mono) -> Mono -> functionMonoToMono, functionJavaMonoToMono, functionKotlinMonoToMono + * 12. (Flux) -> Flux -> functionFluxToFlux, functionJavaFluxToFlux, functionKotlinFluxToFlux + * 13. (Flux) -> Mono -> functionFluxToMono, functionJavaFluxToMono, functionKotlinFluxToMono + * --- Message --- + * 14. (Message) -> Message -> functionMessageToMessage, functionJavaMessageToMessage, functionKotlinMessageToMessage + * 15. suspend (Message) -> Message -> functionSuspendMessageToMessage, functionJavaSuspendMessageToMessage, functionKotlinSuspendMessageToMessage + * 16. (Mono>) -> Mono> -> functionMonoMessageToMonoMessage, functionJavaMonoMessageToMonoMessage, functionKotlinMonoMessageToMonoMessage + * 17. (Flux>) -> Flux> -> functionFluxMessageToFluxMessage, functionJavaFluxMessageToFluxMessage, functionKotlinFluxMessageToFluxMessage + * 18. (Flow>) -> Flow> -> functionFlowMessageToFlowMessage, functionJavaFlowMessageToFlowMessage, functionKotlinFlowMessageToFlowMessage + * 19. suspend (Flow>) -> Flow> -> functionSuspendFlowMessageToFlowMessage, functionJavaSuspendFlowMessageToFlowMessage, functionKotlinSuspendFlowMessageToFlowMessage + * + * @author Adrien Poupard + */ +@FunctionalSpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = [KotlinArityApplication::class] +) +@AutoConfigureWebTestClient +class KotlinFunctionArityBeanTest { + + @Autowired + lateinit var webTestClient: WebTestClient + + @BeforeEach + fun setup() { + this.webTestClient = webTestClient.mutate() + .responseTimeout(Duration.ofSeconds(120)) + .build() + } + + /** + * 1. (T) -> R -> functionPlainToPlain, functionJavaPlainToPlain, functionKotlinPlainToPlain + * Takes a String, returns its length (Int). + * + * --- Input: --- + * POST /functionPlainToPlain + * Content-type: application/json + * "Hello" + * + * --- Output: --- + * Status: 200 OK + * 5 + */ + @ParameterizedTest + @ValueSource(strings = ["functionPlainToPlain", "functionJavaPlainToPlain", "functionKotlinPlainToPlain"]) + fun testFunctionPlainToPlain(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("Hello") + .exchange() + .expectStatus().isOk + .expectBody(Int::class.java) + .isEqualTo(5) + } + + /** + * 2. (T) -> Flow -> functionPlainToFlow, functionJavaPlainToFlow, functionKotlinPlainToFlow + * Takes a String, returns a Flow of its characters. + * + * --- Input: --- + * POST /functionPlainToFlow + * "test" + * + * --- Output: --- + * 200 OK + * ["t","e","s","t"] + */ + @ParameterizedTest + @ValueSource(strings = ["functionPlainToFlow", "functionJavaPlainToFlow", "functionKotlinPlainToFlow"]) + fun testFunctionPlainToFlow(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("test") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("[\"t\",\"e\",\"s\",\"t\"]") + } + + /** + * 3. (Flow) -> R -> functionFlowToPlain, functionJavaFlowToPlain, functionKotlinFlowToPlain + * Takes a Flow of Strings, returns an Int count of items. + * + * --- Input: --- + * POST /functionFlowToPlain + * Content-Type: application/json + * ["one","two","three"] + * + * --- Output: --- + * 200 OK + * [3] + */ + @ParameterizedTest + @ValueSource(strings = ["functionFlowToPlain", "functionJavaFlowToPlain", "functionKotlinFlowToPlain"]) + fun testFunctionFlowToPlain(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("one", "two", "three")) + .exchange() + .expectStatus().isOk + .expectBodyList(Int::class.java) + .hasSize(1) + .contains(3) + } + + /** + * 4. (Flow) -> Flow -> functionFlowToFlow, functionJavaFlowToFlow, functionKotlinFlowToFlow + * Takes a Flow, returns a Flow. + * + * --- Input: --- + * POST /functionFlowToFlow + * Content-Type: application/json + * [1, 2, 3] + * + * --- Output: --- + * 200 OK + * ["1","2","3"] + */ + @ParameterizedTest + @ValueSource(strings = ["functionFlowToFlow", "functionJavaFlowToFlow", "functionKotlinFlowToFlow"]) + fun testFunctionFlowToFlow(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf(1, 2, 3)) + .exchange() + .expectStatus().isOk + .expectBodyList(Int::class.java) + .contains(1, 2, 3) +// .isEqualTo("[\"1\",\"2\",\"3\"]") + } + + /** + * 5. suspend (T) -> R -> functionSuspendPlainToPlain, functionKotlinSuspendPlainToPlain + * Suspending function that takes a String, returns Int (length). + * + * --- Input: --- + * POST /functionSuspendPlainToPlain + * "kotlin" + * + * --- Output: --- + * 200 OK + * [6] + */ + @ParameterizedTest + @ValueSource(strings = ["functionSuspendPlainToPlain", "functionKotlinSuspendPlainToPlain"]) + fun testFunctionSuspendPlainToPlain(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("kotlin") + .exchange() + .expectStatus().isOk + .expectBodyList(Int::class.java) + .hasSize(1) + .contains(6) + } + + /** + * 6. suspend (T) -> Flow -> functionSuspendPlainToFlow, functionKotlinSuspendPlainToFlow + * Takes a String, returns a Flow of its characters. + * + * --- Input: --- + * POST /functionSuspendPlainToFlow + * "demo" + * + * --- Output: --- + * 200 OK + * ["d","e","m","o"] + */ + @ParameterizedTest + @ValueSource(strings = ["functionSuspendPlainToFlow", "functionKotlinSuspendPlainToFlow"]) + fun testFunctionSuspendPlainToFlow(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("demo") + .exchange() + .expectStatus().isOk + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo(listOf("d", "e", "m", "o")) + } + + /** + * 7. suspend (Flow) -> R -> functionSuspendFlowToPlain, functionKotlinSuspendFlowToPlain + * Suspending function that takes a Flow of Strings, returns an Int count. + * + * --- Input: --- + * POST /functionSuspendFlowToPlain + * Content-Type: application/json + * ["alpha","beta"] + * + * --- Output: --- + * 200 OK + * [2] + */ + @ParameterizedTest + @ValueSource(strings = ["functionSuspendFlowToPlain", "functionKotlinSuspendFlowToPlain"]) + fun testFunctionSuspendFlowToPlain(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("alpha", "beta")) + .exchange() + .expectStatus().isOk + .expectBodyList(Int::class.java) + .hasSize(1) + .contains(2) + } + + /** + * 8. suspend (Flow) -> Flow -> functionSuspendFlowToFlow, functionKotlinSuspendFlowToFlow + * Suspending function that takes a Flow, returns a Flow (uppercase). + * + * --- Input: --- + * POST /functionSuspendFlowToFlow + * Content-Type: application/json + * ["abc","xyz"] + * + * --- Output: --- + * 200 OK + * ["ABC","XYZ"] + */ + @ParameterizedTest + @ValueSource(strings = ["functionSuspendFlowToFlow", "functionKotlinSuspendFlowToFlow"]) + fun testFunctionSuspendFlowToFlow(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("abc", "xyz")) + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("[\"ABC\",\"XYZ\"]") + } + + /** + * 9. (T) -> Mono -> functionPlainToMono, functionJavaPlainToMono, functionKotlinPlainToMono + * Takes a String, returns a Mono (length). + * + * --- Input: --- + * POST /functionPlainToMono + * "Reactor" + * + * --- Output: --- + * 200 OK + * 7 + */ + @ParameterizedTest + @ValueSource(strings = ["functionPlainToMono", "functionJavaPlainToMono", "functionKotlinPlainToMono"]) + fun testFunctionPlainToMono(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("Reactor") + .exchange() + .expectStatus().isOk + .expectBody(Int::class.java) + .isEqualTo(7) + } + + /** + * 10. (T) -> Flux -> functionPlainToFlux, functionJavaPlainToFlux, functionKotlinPlainToFlux + * Takes a String, returns a Flux (characters). + * + * --- Input: --- + * POST /functionPlainToFlux + * "Flux" + * + * --- Output: --- + * 200 OK + * ["F","l","u","x"] + */ + @ParameterizedTest + @ValueSource(strings = ["functionPlainToFlux", "functionJavaPlainToFlux", "functionKotlinPlainToFlux"]) + fun testFunctionPlainToFlux(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("Flux") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("[\"F\",\"l\",\"u\",\"x\"]") + } + + /** + * 11. (Mono) -> Mono -> functionMonoToMono, functionJavaMonoToMono, functionKotlinMonoToMono + * Takes a Mono, returns a Mono (uppercase). + * + * --- Input: --- + * POST /functionMonoToMono + * "input mono" + * + * --- Output: --- + * 200 OK + * "INPUT MONO" + */ + @ParameterizedTest + @ValueSource(strings = ["functionMonoToMono", "functionJavaMonoToMono", "functionKotlinMonoToMono"]) + fun testFunctionMonoToMono(name: String) { + webTestClient.post() + .uri("/$name") + .bodyValue("input mono") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("INPUT MONO") + } + + /** + * 12. (Flux) -> Flux -> functionFluxToFlux, functionJavaFluxToFlux, functionKotlinFluxToFlux + * Takes a Flux, returns a Flux (lengths). + * + * --- Input: --- + * POST /functionFluxToFlux + * Content-Type: application/json + * ["one","three","five"] + * + * --- Output: --- + * 200 OK + * [3,5,4] + */ + @ParameterizedTest + @ValueSource(strings = ["functionFluxToFlux", "functionJavaFluxToFlux", "functionKotlinFluxToFlux"]) + fun testFunctionFluxToFlux(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("one", "three", "five")) + .exchange() + .expectStatus().isOk + .expectBodyList(Int::class.java) + .contains(3, 5, 4) + } + + /** + * 13. (Flux) -> Mono -> functionFluxToMono, functionJavaFluxToMono, functionKotlinFluxToMono + * Takes a Flux, returns a Mono (count). + * + * --- Input: --- + * POST /functionFluxToMono + * Content-Type: application/json + * ["a","b","c","d"] + * + * --- Output: --- + * 200 OK + * 4 + */ + @ParameterizedTest + @ValueSource(strings = ["functionFluxToMono", "functionJavaFluxToMono", "functionKotlinFluxToMono"]) + fun testFunctionFluxToMono(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("a", "b", "c", "d")) + .exchange() + .expectStatus().isOk + .expectBodyList(Int::class.java) + .contains(4) + } + + /** + * 14. (Message) -> Message -> functionMessageToMessage, functionJavaMessageToMessage, functionKotlinMessageToMessage + * Takes Message, returns Message (length), adds header. + * + * --- Input: --- + * POST /functionMessageToMessage + * Header: myHeader=myValue + * "message test" + * + * --- Output: --- + * 200 OK + * Header: processed=true + * Header: myHeader=myValue + * 12 + */ + @ParameterizedTest + @ValueSource(strings = ["functionMessageToMessage", "functionJavaMessageToMessage", "functionKotlinMessageToMessage"]) + fun testFunctionMessageToMessage(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .header("myHeader", "myValue") + .bodyValue("\"message test\"") + .exchange() + .expectStatus().isOk + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectHeader().valueEquals("processed", "true") + .expectHeader().exists("myHeader") + .expectBody(Int::class.java) + .isEqualTo(14) + } + + /** + * 15. suspend (Message) -> Message -> functionSuspendMessageToMessage, functionKotlinSuspendMessageToMessage + * Suspending function takes Message, returns Message. + * + * --- Input: --- + * POST /functionSuspendMessageToMessage + * Header: id= + * Header: another=value + * "suspend msg" + * + * --- Output: --- + * 200 OK + * Header: suspend-processed=true + * Header: original-id= + * Header: another=value + * 22 + */ + @ParameterizedTest + @ValueSource(strings = ["functionSuspendMessageToMessage", "functionKotlinSuspendMessageToMessage"]) + fun testFunctionSuspendMessageToMessage(name: String) { + val inputId = UUID.randomUUID().toString() + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .header("original-id", inputId) + .header("another", "value") + .bodyValue("\"suspend msg\"") + .exchange() + .expectStatus().isOk + .expectHeader().valueEquals("suspend-processed", "true") + .expectHeader().valueEquals("original-id", inputId) + .expectHeader().exists("another") + .expectBodyList(Int::class.java) + .contains(26) + } + + /** + * 16. (Mono>) -> Mono> -> functionMonoMessageToMonoMessage, functionJavaMonoMessageToMonoMessage, functionKotlinMonoMessageToMonoMessage + * Takes Mono>, returns Mono> (hashcode). + * + * --- Input: --- + * POST /functionMonoMessageToMonoMessage + * Header: monoHeader=monoValue + * "test mono message" + * + * --- Output: --- + * 200 OK + * Header: mono-processed=true + * Header: monoHeader=monoValue + * + */ + @ParameterizedTest + @ValueSource(strings = ["functionMonoMessageToMonoMessage", "functionJavaMonoMessageToMonoMessage", "functionKotlinMonoMessageToMonoMessage"]) + fun testFunctionMonoMessageToMonoMessage(name: String) { + val inputPayload = "test mono message" + val expectedPayload = "test mono message".hashCode() + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .header("monoHeader", "monoValue") + .bodyValue(inputPayload) + .exchange() + .expectStatus().isOk + .expectHeader().valueEquals("mono-processed", "true") + .expectHeader().exists("monoHeader") + .expectBody(Int::class.java) + .isEqualTo(expectedPayload) + } + + /** + * 17. (Flux>) -> Flux> -> functionFluxMessageToFluxMessage, functionJavaFluxMessageToFluxMessage, functionKotlinFluxMessageToFluxMessage + * Takes Flux>, returns Flux> (uppercase). + * + * --- Input: --- + * POST /functionFluxMessageToFluxMessage + * Content-Type: application/json + * ["msg one","msg two"] + * + * --- Output: --- + * 200 OK + * ["MSG ONE", "MSG TWO"] + * (Headers flux-processed=true on each message) + */ + @ParameterizedTest + @ValueSource(strings = ["functionFluxMessageToFluxMessage", "functionJavaFluxMessageToFluxMessage", "functionKotlinFluxMessageToFluxMessage"]) + fun testFunctionFluxMessageToFluxMessage(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("msg one", "msg two")) + .exchange() + .expectStatus().isOk + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo(listOf("MSG ONE", "MSG TWO")) + } + + /** + * 18. (Flow>) -> Flow> -> functionFlowMessageToFlowMessage, functionJavaFlowMessageToFlowMessage, functionKotlinFlowMessageToFlowMessage + * Takes Flow>, returns Flow> (reversed). + * + * --- Input: --- + * POST /functionFlowMessageToFlowMessage + * Content-Type: application/json + * ["flow one", "flow two"] + * + * --- Output: --- + * 200 OK + * ["eno wolf", "owt wolf"] + * (Headers flow-processed=true on each message) + */ + @ParameterizedTest + @ValueSource(strings = ["functionFlowMessageToFlowMessage", "functionJavaFlowMessageToFlowMessage", "functionKotlinFlowMessageToFlowMessage"]) + fun testFunctionFlowMessageToFlowMessage(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(listOf("flow one", "flow two")) + .exchange() + .expectStatus().isOk + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo(listOf("eno wolf", "owt wolf")) + } + + /** + * 19. suspend (Flow>) -> Flow> -> functionSuspendFlowMessageToFlowMessage, functionKotlinSuspendFlowMessageToFlowMessage + * Suspending fn takes Flow>, returns Flow> (appended). + * + * --- Input: --- + * POST /functionSuspendFlowMessageToFlowMessage + * Content-Type: application/json + * ["sus flow one", "sus flow two"] + * + * --- Output: --- + * 200 OK + * ["sus flow one SUSPEND", "sus flow two SUSPEND"] + * (Headers suspend-flow-processed=true on each message) + */ + @ParameterizedTest + @ValueSource(strings = ["functionSuspendFlowMessageToFlowMessage", "functionKotlinSuspendFlowMessageToFlowMessage"]) + fun testFunctionSuspendFlowMessageToFlowMessage(name: String) { + webTestClient.post() + .uri("/$name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(listOf("sus flow one", "sus flow two")) + .exchange() + .expectStatus().isOk + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo(listOf("sus flow one SUSPEND", "sus flow two SUSPEND")) + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinSupplierArityBeanTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinSupplierArityBeanTest.kt new file mode 100644 index 000000000..c5b39acbe --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinSupplierArityBeanTest.kt @@ -0,0 +1,355 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.kotlin.web + +import java.time.Duration +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.function.context.test.FunctionalSpringBootTest +import org.springframework.cloud.function.kotlin.arity.KotlinArityApplication +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.MediaType +import org.springframework.test.web.reactive.server.WebTestClient + +/** + * Test class for verifying the Kotlin Supplier examples in [KotlinSupplierExamples]. + * Each bean is exposed at "/{beanName}" by Spring Cloud Function. + * + * ## Suppliers Tested: + * --- Coroutine --- + * 1. () -> R -> supplierPlain + * 2. () -> Flow -> supplierFlow + * 3. suspend () -> R -> supplierSuspendPlain + * 4. suspend () -> Flow -> supplierSuspendFlow + * --- Reactor --- + * 5. () -> Mono -> supplierMono + * 6. () -> Flux -> supplierFlux + * --- Message --- + * 7. () -> Message -> supplierMessage + * 8. () -> Mono> -> supplierMonoMessage + * 9. suspend () -> Message -> supplierSuspendMessage + * 10. () -> Flux> -> supplierFluxMessage + * 11. () -> Flow> -> supplierFlowMessage + * 12. suspend () -> Flow> -> supplierSuspendFlowMessage + * + * @author Adrien Poupard + */ +@FunctionalSpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = [KotlinArityApplication::class] +) +@AutoConfigureWebTestClient +class KotlinSupplierArityBeanTest { + + @Autowired + lateinit var webTestClient: WebTestClient + + @BeforeEach + fun setup() { + this.webTestClient = webTestClient.mutate() + .responseTimeout(Duration.ofSeconds(120)) + .build() + } + + /** + * 1. () -> R -> supplierPlain, supplierJavaPlain, supplierKotlinPlain + * No input, returns an Int. + * + * --- Input: --- + * GET /supplierPlain + * + * --- Output: --- + * 200 OK + * 42 + */ + @ParameterizedTest + @ValueSource(strings = ["supplierPlain", "supplierJavaPlain", "supplierKotlinPlain"]) + fun testSupplierPlain(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(Int::class.java) + .isEqualTo(42) + } + + /** + * 2. () -> Flow -> supplierFlow, supplierJavaFlow, supplierKotlinFlow + * No input, returns a Flow of Strings. + * + * --- Input: --- + * GET /supplierFlow + * + * --- Output: --- + * 200 OK + * ["A","B","C"] + */ + @ParameterizedTest + @ValueSource(strings = ["supplierFlow", "supplierJavaFlow", "supplierKotlinFlow"]) + fun testSupplierFlow(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("[\"A\",\"B\",\"C\"]") + } + + /** + * 3. suspend () -> R -> supplierSuspendPlain, supplierKotlinSuspendPlain + * Suspending supplier that returns a single String. + * + * --- Input: --- + * GET /supplierSuspendPlain + * + * --- Output: --- + * 200 OK + * ["Hello from suspend"] + */ + @ParameterizedTest + @ValueSource(strings = ["supplierSuspendPlain", "supplierKotlinSuspendPlain"]) + fun testSupplierSuspendPlain(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("[\"Hello from suspend\"]") + } + + /** + * 4. suspend () -> Flow -> supplierSuspendFlow, supplierKotlinSuspendFlow + * Suspending supplier that returns a Flow of Strings. + * + * --- Input: --- + * GET /supplierSuspendFlow + * + * --- Output: --- + * 200 OK + * ["x","y","z"] + */ + @ParameterizedTest + @ValueSource(strings = ["supplierSuspendFlow", "supplierKotlinSuspendFlow"]) + fun testSupplierSuspendFlow(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("[\"x\",\"y\",\"z\"]") + } + + /** + * 5. () -> Mono -> supplierMono, supplierJavaMono, supplierKotlinMono + * Supplier that returns Mono. + * + * --- Input: --- + * GET /supplierMono + * + * --- Output: --- + * 200 OK + * "Hello from Mono" + */ + @ParameterizedTest + @ValueSource(strings = ["supplierMono", "supplierJavaMono", "supplierKotlinMono"]) + fun testSupplierMono(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("Hello from Mono") + } + + /** + * 6. () -> Flux -> supplierFlux, supplierJavaFlux, supplierKotlinFlux + * Supplier that returns Flux. + * + * --- Input: --- + * GET /supplierFlux + * + * --- Output: --- + * 200 OK + * ["Alpha","Beta","Gamma"] + */ + @ParameterizedTest + @ValueSource(strings = ["supplierFlux", "supplierJavaFlux", "supplierKotlinFlux"]) + fun testSupplierFlux(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(String::class.java) + .isEqualTo("[\"Alpha\",\"Beta\",\"Gamma\"]") + } + + /** + * 7. () -> Message -> supplierMessage, supplierJavaMessage, supplierKotlinMessage + * Supplier that returns Message with a header. + * + * --- Input: --- + * GET /supplierMessage + * + * --- Output: --- + * 200 OK + * Header: messageId= + * "Hello from Message" + */ + @ParameterizedTest + @ValueSource(strings = ["supplierMessage", "supplierJavaMessage", "supplierKotlinMessage"]) + fun testSupplierMessage(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectHeader().exists("messageId") + .expectBody(String::class.java) + .isEqualTo("Hello from Message") + } + + /** + * 8. () -> Mono> -> supplierMonoMessage, supplierJavaMonoMessage, supplierKotlinMonoMessage + * Supplier that returns Mono>. + * + * --- Input: --- + * GET /supplierMonoMessage + * + * --- Output: --- + * 200 OK + * Header: monoMessageId= + * Header: source=mono + * "Hello from Mono Message" + */ + @ParameterizedTest + @ValueSource(strings = ["supplierMonoMessage", "supplierJavaMonoMessage", "supplierKotlinMonoMessage"]) + fun testSupplierMonoMessage(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectHeader().exists("monoMessageId") + .expectHeader().valueEquals("source", "mono") + .expectBody(String::class.java) + .isEqualTo("Hello from Mono Message") + } + + /** + * 9. suspend () -> Message -> supplierSuspendMessage, supplierKotlinSuspendMessage + * Suspending supplier that returns Message. + * + * --- Input: --- + * GET /supplierSuspendMessage + * + * --- Output: --- + * 200 OK + * Header: suspendMessageId= + * Header: wasSuspended=true + * "Hello from Suspend Message" + */ + @ParameterizedTest + @ValueSource(strings = ["supplierSuspendMessage", "supplierKotlinSuspendMessage"]) + fun testSupplierSuspendMessage(name: String) { + webTestClient.get() + .uri("/$name") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + .expectHeader().exists("suspendMessageId") + .expectHeader().valueEquals("wasSuspended", "true") + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo(listOf("Hello from Suspend Message")) + } + + /** + * 10. () -> Flux> -> supplierFluxMessage, supplierJavaFluxMessage, supplierKotlinFluxMessage + * Supplier that returns Flux> with headers. + * + * --- Input: --- + * GET /supplierFluxMessage + * + * --- Output: --- + * 200 OK + * ["Msg1", "Msg2"] + * (Headers fluxMessageId= on each message) + */ + @ParameterizedTest + @ValueSource(strings = ["supplierFluxMessage", "supplierJavaFluxMessage", "supplierKotlinFluxMessage"]) + fun testSupplierFluxMessage(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo(listOf("Msg1", "Msg2")) + } + + /** + * 11. () -> Flow> -> supplierFlowMessage, supplierJavaFlowMessage, supplierKotlinFlowMessage + * Supplier that returns Flow> with headers. + * + * --- Input: --- + * GET /supplierFlowMessage + * + * --- Output: --- + * 200 OK + * ["FlowMsg1", "FlowMsg2"] + * (Headers flowMessageId= on each message) + */ + @ParameterizedTest + @ValueSource(strings = ["supplierFlowMessage", "supplierJavaFlowMessage", "supplierKotlinFlowMessage"]) + fun testSupplierFlowMessage(name: String) { + webTestClient.get() + .uri("/$name") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo( + listOf( + "FlowMsg1", + "FlowMsg2" + ) + ) + } + + /** + * 12. suspend () -> Flow> -> supplierSuspendFlowMessage, supplierKotlinSuspendFlowMessage + * Suspending supplier that returns Flow> with headers. + * + * --- Input: --- + * GET /supplierSuspendFlowMessage + * + * --- Output: --- + * 200 OK + * ["SuspendFlowMsg1", "SuspendFlowMsg2"] + * (Headers suspendFlowMessageId= on each message) + */ + @ParameterizedTest + @ValueSource(strings = ["supplierSuspendFlowMessage", "supplierKotlinSuspendFlowMessage"]) + fun testSupplierSuspendFlowMessage(name: String) { + webTestClient.get() + .uri("/$name") + .exchange() + .expectStatus().isOk + .expectBody(ParameterizedTypeReference.forType>(List::class.java)) + .isEqualTo(listOf("SuspendFlowMsg1", "SuspendFlowMsg2")) + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerFlowWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerFlowWrapperTest.kt new file mode 100644 index 000000000..f29a385de --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerFlowWrapperTest.kt @@ -0,0 +1,100 @@ + +package org.springframework.cloud.function.kotlin.wrapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.runBlocking +import reactor.core.publisher.Flux +import kotlin.Unit +import org.springframework.cloud.function.context.wrapper.KotlinConsumerFlowWrapper + +/* + * @author Adrien Poupard + */ +@OptIn(ExperimentalStdlibApi::class) +class KotlinConsumerFlowWrapperTest { + + // Sample consumer function that accepts a Flow + private val collectedItems = mutableListOf() + + private val sampleConsumer: (Flow) -> Unit = { flow -> + runBlocking { + collectedItems.clear() + flow.collect { collectedItems.add(it) } + } + } + + @Test + fun `test isValid with valid consumer flow type`() { + // Given + val functionType = typeOf<(Flow) -> Unit>().javaType + val types = arrayOf(typeOf>().javaType, typeOf().javaType) + + // When + val result = KotlinConsumerFlowWrapper.isValid(functionType, types) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `test isValid with invalid consumer type`() { + // Given + val functionType = typeOf<(String) -> Unit>().javaType + val types = arrayOf(typeOf().javaType, typeOf().javaType) + + // When + val result = KotlinConsumerFlowWrapper.isValid(functionType, types) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `test asRegistrationFunction creates wrapper correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf(typeOf>().javaType, typeOf().javaType) + + // When + val wrapper = KotlinConsumerFlowWrapper.asRegistrationFunction(functionName, sampleConsumer, types) + + // Then + assertThat(wrapper).isNotNull + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.resolvableType).isNotNull + } + + @Test + fun `test accept method processes Flux correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf(typeOf>().javaType, typeOf().javaType) + val wrapper = KotlinConsumerFlowWrapper.asRegistrationFunction(functionName, sampleConsumer, types) + val inputFlux = Flux.just("test1", "test2", "test3") as Flux + + // When + wrapper.accept(inputFlux) + + // Then + assertThat(collectedItems).containsExactly("test1", "test2", "test3") + } + + @Test + fun `test invoke method processes Flux correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf(typeOf>().javaType, typeOf().javaType) + val wrapper = KotlinConsumerFlowWrapper.asRegistrationFunction(functionName, sampleConsumer, types) + val inputFlux = Flux.just("test4", "test5", "test6") as Flux + + // When + wrapper.accept(inputFlux) + + // Then + assertThat(collectedItems).containsExactly("test4", "test5", "test6") + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerPlainWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerPlainWrapperTest.kt new file mode 100644 index 000000000..f71ab5ab4 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerPlainWrapperTest.kt @@ -0,0 +1,95 @@ + +package org.springframework.cloud.function.kotlin.wrapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.lang.reflect.Type +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import kotlin.Unit +import org.springframework.cloud.function.context.wrapper.KotlinConsumerPlainWrapper + +/* + * @author Adrien Poupard + */ +@OptIn(ExperimentalStdlibApi::class) +class KotlinConsumerPlainWrapperTest { + + // Sample consumer function that accepts a plain object + private var lastConsumedValue: String? = null + + private val sampleConsumer: (String) -> Unit = { input -> + lastConsumedValue = input + } + + @Test + fun `test isValid with valid consumer plain type`() { + // Given + val functionType = typeOf<(String) -> Unit>().javaType + val types = arrayOf(typeOf().javaType, typeOf().javaType) + + // When + val result = KotlinConsumerPlainWrapper.isValid(functionType, types) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `test isValid with invalid consumer type (flow)`() { + // Given + val functionType = typeOf<(kotlinx.coroutines.flow.Flow) -> Unit>().javaType + val types = arrayOf(typeOf>().javaType, typeOf().javaType) + + // When + val result = KotlinConsumerPlainWrapper.isValid(functionType, types) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `test asRegistrationFunction creates wrapper correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf(typeOf().javaType, typeOf().javaType) + + // When + val wrapper = KotlinConsumerPlainWrapper.asRegistrationFunction(functionName, sampleConsumer, types) + + // Then + assertThat(wrapper).isNotNull + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isNotNull + } + + @Test + fun `test accept method processes input correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf(typeOf().javaType, typeOf().javaType) + val wrapper = KotlinConsumerPlainWrapper.asRegistrationFunction(functionName, sampleConsumer, types) + val input = "test input" + + // When + wrapper.accept(input) + + // Then + assertThat(lastConsumedValue).isEqualTo(input) + } + + @Test + fun `test accept method with Function1 implementation`() { + // Given + val functionName = "testFunction" + val types = arrayOf(typeOf().javaType, typeOf().javaType) + val wrapper = KotlinConsumerPlainWrapper.asRegistrationFunction(functionName, sampleConsumer, types) + val input = "test with Function1" + + // When + wrapper.accept(input) + + // Then + assertThat(lastConsumedValue).isEqualTo(input) + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendFlowWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendFlowWrapperTest.kt new file mode 100644 index 000000000..8f8eab471 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendFlowWrapperTest.kt @@ -0,0 +1,130 @@ + +package org.springframework.cloud.function.kotlin.wrapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.lang.reflect.Type +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import org.springframework.core.ResolvableType +import kotlin.Unit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import reactor.core.publisher.Flux +import org.springframework.cloud.function.context.wrapper.KotlinConsumerSuspendFlowWrapper + +/* + * @author Adrien Poupard + */ +@OptIn(ExperimentalStdlibApi::class) +class KotlinConsumerSuspendFlowWrapperTest { + + // Sample suspend consumer function that accepts Flow + private var lastConsumedValues = mutableListOf() + + private val sampleSuspendFlowConsumer: suspend (Flow) -> Unit = { flow -> + flow.collect { value -> + lastConsumedValues.add(value) + } + } + + @Test + fun `test isValid with valid suspend flow consumer type`() { + // Given + val functionType = sampleSuspendFlowConsumer.javaClass.genericInterfaces[0] + val types = arrayOf( + typeOf>().javaType, + typeOf>().javaType, + typeOf().javaType + ) + + // When + val result = KotlinConsumerSuspendFlowWrapper.isValid(functionType, types) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `test isValid with invalid consumer type (not flow)`() { + // Given + // Create a sample suspend consumer that doesn't use Flow + val sampleNonFlowConsumer: suspend (String) -> Unit = { _ -> } + val functionType = sampleNonFlowConsumer.javaClass.genericInterfaces[0] + val types = arrayOf( + typeOf().javaType, + typeOf>().javaType, + typeOf().javaType + ) + + // When + val result = KotlinConsumerSuspendFlowWrapper.isValid(functionType, types) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `test asRegistrationFunction creates wrapper correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf( + typeOf>().javaType, + typeOf>().javaType, + typeOf().javaType + ) + + // When + val wrapper = KotlinConsumerSuspendFlowWrapper.asRegistrationFunction(functionName, sampleSuspendFlowConsumer, types) + + // Then + assertThat(wrapper).isNotNull + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isNotNull + } + + @Test + fun `test accept method processes flow input correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf( + typeOf>().javaType, + typeOf>().javaType, + typeOf().javaType + ) + val wrapper = KotlinConsumerSuspendFlowWrapper.asRegistrationFunction(functionName, sampleSuspendFlowConsumer, types) + val input = Flux.just("test1", "test2", "test3") as Flux + lastConsumedValues.clear() + + // When + wrapper.accept(input) + + // Wait a bit for the async operation to complete + Thread.sleep(100) + + // Then + assertThat(lastConsumedValues).contains("test1", "test2", "test3") + } + + @Test + fun `test constructor with type parameter`() { + // Given + val functionName = "testFunction" + val type = ResolvableType.forClassWithGenerics( + java.util.function.Consumer::class.java, + ResolvableType.forClassWithGenerics( + reactor.core.publisher.Flux::class.java, + ResolvableType.forClass(String::class.java) + ) + ) + + // When + val wrapper = KotlinConsumerSuspendFlowWrapper(sampleSuspendFlowConsumer, type, functionName) + + // Then + assertThat(wrapper).isNotNull + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isEqualTo(type) + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendPlainWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendPlainWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendPlainWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToFlowWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToFlowWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToFlowWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToPlainWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToPlainWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToPlainWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToFlowWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToFlowWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToFlowWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToPlainWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToPlainWrapperTest.kt new file mode 100644 index 000000000..6e1e89353 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToPlainWrapperTest.kt @@ -0,0 +1,126 @@ +package org.springframework.cloud.function.kotlin.wrapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.lang.reflect.Type +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import org.springframework.cloud.function.context.wrapper.KotlinFunctionPlainToPlainWrapper +import org.springframework.core.ResolvableType + +/** + * @author Adrien Poupard + */ +@OptIn(ExperimentalStdlibApi::class) +class KotlinFunctionPlainToPlainWrapperTest { + + // Sample function that transforms a String to an Int + private val sampleFunction: (String) -> Int = { input -> + input.length + } + + @Test + fun `test isValid with valid object to object function type`() { + // Given + val functionType = typeOf<(String) -> Int>().javaType + val types = arrayOf( + typeOf().javaType, + typeOf().javaType + ) + + // When + val result = KotlinFunctionPlainToPlainWrapper.isValid(functionType, types) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `test asRegistrationFunction creates wrapper correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf( + typeOf().javaType, + typeOf().javaType + ) + + // When + val wrapper = KotlinFunctionPlainToPlainWrapper.asRegistrationFunction( + functionName, + sampleFunction, + types + ) + + // Then + assertThat(wrapper).isNotNull + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isNotNull + } + + @Test + fun `test apply method processes input correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf( + typeOf().javaType, + typeOf().javaType + ) + val wrapper = KotlinFunctionPlainToPlainWrapper.asRegistrationFunction( + functionName, + sampleFunction, + types + ) + val input = "test input" + + // When + val result = wrapper.apply(input) + + // Then + assertThat(result).isEqualTo(10) // "test input".length = 10 + } + + @Test + fun `test invoke method processes input correctly`() { + // Given + val functionName = "testFunction" + val types = arrayOf( + typeOf().javaType, + typeOf().javaType + ) + val wrapper = KotlinFunctionPlainToPlainWrapper.asRegistrationFunction( + functionName, + sampleFunction, + types + ) + val input = "another test" + + // When + val result = wrapper.invoke(input) + + // Then + assertThat(result).isEqualTo(12) // "another test".length = 12 + } + + @Test + fun `test constructor with type parameter`() { + // Given + val functionName = "testFunction" + val type = ResolvableType.forClassWithGenerics( + java.util.function.Function::class.java, + ResolvableType.forClass(String::class.java), + ResolvableType.forClass(Int::class.java) + ) + // When + val wrapper = + KotlinFunctionPlainToPlainWrapper( + sampleFunction, + type, + functionName + ) + + // Then + assertThat(wrapper).isNotNull + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isEqualTo(type) + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToFlowWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToFlowWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToFlowWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToPlainWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToPlainWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToPlainWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToFlowWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToFlowWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToFlowWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToPlainWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToPlainWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToPlainWrapperTest.kt @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionWrapperTest.kt new file mode 100644 index 000000000..0692c2d2e --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionWrapperTest.kt @@ -0,0 +1,72 @@ +package org.springframework.cloud.function.kotlin.wrapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.cloud.function.context.wrapper.KotlinFunctionWrapper +import org.springframework.core.ResolvableType + +/* + * @author Adrien Poupard + */ +class KotlinFunctionWrapperTest { + + // Simple implementation of KotlinFunctionWrapper for testing + private class TestKotlinFunctionWrapper( + private val type: ResolvableType, + private val name: String + ) : KotlinFunctionWrapper { + override fun getResolvableType(): ResolvableType = type + override fun getName(): String = name + } + + @Test + fun `test getName returns correct name`() { + // Given + val expectedName = "testFunction" + val type = ResolvableType.forClass(String::class.java) + val wrapper = TestKotlinFunctionWrapper(type, expectedName) + + // When + val actualName = wrapper.getName() + + // Then + assertThat(actualName).isEqualTo(expectedName) + } + + @Test + fun `test getResolvableType returns correct type`() { + // Given + val name = "testFunction" + val expectedType = ResolvableType.forClass(String::class.java) + val wrapper = TestKotlinFunctionWrapper(expectedType, name) + + // When + val actualType = wrapper.getResolvableType() + + // Then + assertThat(actualType).isEqualTo(expectedType) + } + + @Test + fun `test implementation with complex type`() { + // Given + val name = "complexFunction" + val expectedType = ResolvableType.forClassWithGenerics( + java.util.function.Function::class.java, + ResolvableType.forClass(String::class.java), + ResolvableType.forClassWithGenerics( + reactor.core.publisher.Flux::class.java, + ResolvableType.forClass(Int::class.java) + ) + ) + val wrapper = TestKotlinFunctionWrapper(expectedType, name) + + // When + val actualType = wrapper.getResolvableType() + val actualName = wrapper.getName() + + // Then + assertThat(actualType).isEqualTo(expectedType) + assertThat(actualName).isEqualTo(name) + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierFlowWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierFlowWrapperTest.kt new file mode 100644 index 000000000..6a2747fb0 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierFlowWrapperTest.kt @@ -0,0 +1,140 @@ + + +package org.springframework.cloud.function.kotlin.wrapper + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import org.springframework.cloud.function.context.wrapper.KotlinSupplierFlowWrapper +import org.springframework.core.ResolvableType +import reactor.core.publisher.Flux +import java.util.function.Supplier + +/** + * @author Adrien Poupard + */ +@OptIn(ExperimentalStdlibApi::class) +class KotlinSupplierFlowWrapperTest { + + // Sample supplier function that returns a Flow + private val sampleSupplier: () -> Flow = { + flow { + emit("Hello") + emit("from") + emit("flow") + emit("supplier") + } + } + + @Test + fun `test isValid with valid supplier flow type`() { + // Given + val functionType = typeOf<() -> Flow>().javaType + val types = arrayOf(typeOf>().javaType) + + // When + val result = KotlinSupplierFlowWrapper.isValid(functionType, types) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `test isValid with invalid supplier type (not a flow)`() { + // Given + val functionType = typeOf<() -> String>().javaType + val types = arrayOf(typeOf().javaType) + + // When + val result = KotlinSupplierFlowWrapper.isValid(functionType, types) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `test asRegistrationFunction creates wrapper correctly`() { + // Given + val functionName = "testFlowSupplier" + val types = arrayOf(typeOf>().javaType) + + // When + val wrapper = KotlinSupplierFlowWrapper.asRegistrationFunction(functionName, sampleSupplier, types) + + // Then + assertThat(wrapper != null).isTrue() + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType() != null).isTrue() + } + + @Test + fun `test get method returns correct Flux`() { + // Given + val functionName = "testFlowSupplier" + val types = arrayOf(typeOf>().javaType) + val wrapper = KotlinSupplierFlowWrapper.asRegistrationFunction(functionName, sampleSupplier, types) + + // When + val result = wrapper.get() + + // Then + assertThat(result).isInstanceOf(Flux::class.java) + val items = result.collectList().block() + assertThat(items).containsExactly("Hello", "from", "flow", "supplier") + } + + @Test + fun `test invoke method returns correct Flow`() { + // Given + val functionName = "testFlowSupplier" + val types = arrayOf(typeOf>().javaType) + val wrapper = KotlinSupplierFlowWrapper.asRegistrationFunction(functionName, sampleSupplier, types) + + // When + val result = wrapper.invoke() + + // Then + assertThat(result).isNotNull + // Collect items from Flow + val items = runBlocking { + result.toList() + } + assertThat(items).containsExactly("Hello", "from", "flow", "supplier") + } + + @Test + fun `test constructor with type parameter`() { + // Given + val functionName = "testFlowSupplier" + val fluxType = ResolvableType.forClassWithGenerics( + Flux::class.java, + ResolvableType.forClass(String::class.java) + ) + val type = ResolvableType.forClassWithGenerics( + Supplier::class.java, + fluxType + ) + + // When + val wrapper = KotlinSupplierFlowWrapper( + sampleSupplier, + type, + functionName + ) + + // Then + assertThat(wrapper != null).isTrue() + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isEqualTo(type) + + // Test get method + val flux = wrapper.get() + val items = flux.collectList().block() + assertThat(items).containsExactly("Hello", "from", "flow", "supplier") + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierPlainWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierPlainWrapperTest.kt new file mode 100644 index 000000000..3f8732e2a --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierPlainWrapperTest.kt @@ -0,0 +1,113 @@ + + +package org.springframework.cloud.function.kotlin.wrapper + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import org.springframework.cloud.function.context.wrapper.KotlinSupplierPlainWrapper +import org.springframework.core.ResolvableType +import java.util.function.Supplier + +/** + * @author Adrien Poupard + */ +@OptIn(ExperimentalStdlibApi::class) +class KotlinSupplierPlainWrapperTest { + + // Sample supplier function that returns a plain object + private val sampleSupplier: () -> String = { + "Hello from supplier" + } + + @Test + fun `test isValid with valid supplier plain type`() { + // Given + val functionType = typeOf<() -> String>().javaType + val types = arrayOf(typeOf().javaType) + + // When + val result = KotlinSupplierPlainWrapper.isValid(functionType, types) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `test isValid with invalid supplier type (not a supplier)`() { + // Given + val functionType = typeOf<(String) -> String>().javaType + val types = arrayOf(typeOf().javaType, typeOf().javaType) + + // When + val result = KotlinSupplierPlainWrapper.isValid(functionType, types) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `test asRegistrationFunction creates wrapper correctly`() { + // Given + val functionName = "testSupplier" + val types = arrayOf(typeOf().javaType) + + // When + val wrapper = KotlinSupplierPlainWrapper.asRegistrationFunction(functionName, sampleSupplier, types) + + // Then + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isNotNull + } + + @Test + fun `test get method returns correct value`() { + // Given + val functionName = "testSupplier" + val types = arrayOf(typeOf().javaType) + val wrapper = KotlinSupplierPlainWrapper.asRegistrationFunction(functionName, sampleSupplier, types) + + // When + val result = wrapper.get() + + // Then + assertThat(result).isEqualTo("Hello from supplier") + } + + @Test + fun `test apply method with empty input returns supplier result`() { + // Given + val functionName = "testSupplier" + val types = arrayOf(typeOf().javaType) + val wrapper = KotlinSupplierPlainWrapper.asRegistrationFunction(functionName, sampleSupplier, types) + + // When + val result = wrapper.apply(null) + + // Then + assertThat(result).isEqualTo("Hello from supplier") + } + + @Test + fun `test constructor with type parameter`() { + // Given + val functionName = "testSupplier" + val type = ResolvableType.forClassWithGenerics( + Supplier::class.java, + ResolvableType.forClass(String::class.java) + ) + + // When + val wrapper = KotlinSupplierPlainWrapper( + sampleSupplier, + type, + functionName + ) + + // Then + assertThat(wrapper.getName()).isEqualTo(functionName) + assertThat(wrapper.getResolvableType()).isEqualTo(type) + assertThat(wrapper.get()).isEqualTo("Hello from supplier") + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierSuspendWrapperTest.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierSuspendWrapperTest.kt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierSuspendWrapperTest.kt @@ -0,0 +1 @@ +