From 029fc74444796072ccbc0c46eac8e309c30dd73d Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 8 May 2025 15:16:51 +0200 Subject: [PATCH 1/6] Test: Add arity tests for Kotlin functions, consumers, and suppliers This commit introduces a comprehensive suite of tests to cover all possible declarations of Kotlin functions, consumers, and suppliers within the `spring-cloud-function-kotlin` module. The primary goal of these tests is to explore the various declaration combinations and identify potential areas for improvement and enhanced support in the framework. Signed-off-by: Adrien Poupard --- .../kotlin/arity/KotlinArityApplication.kt | 12 + .../arity/KotlinAritySupplierComponent.kt | 188 ++++++ .../kotlin/arity/KotlinConsumerArityBean.kt | 150 +++++ .../arity/KotlinConsumerArityComponent.kt | 173 +++++ .../kotlin/arity/KotlinConsumerArityJava.kt | 135 ++++ .../kotlin/arity/KotlinFunctionArityBean.kt | 218 +++++++ .../arity/KotlinFunctionArityComponent.kt | 256 ++++++++ .../kotlin/arity/KotlinFunctionArityJava.kt | 193 ++++++ .../kotlin/arity/KotlinSupplierArityBean.kt | 165 +++++ .../kotlin/arity/KotlinSupplierArityJava.kt | 144 +++++ .../kotlin/web/KotlinConsumerArityBeanTest.kt | 383 +++++++++++ .../kotlin/web/KotlinFunctionArityBeanTest.kt | 592 ++++++++++++++++++ .../kotlin/web/KotlinSupplierArityBeanTest.kt | 355 +++++++++++ 13 files changed, 2964 insertions(+) create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinArityApplication.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinAritySupplierComponent.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityBean.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityComponent.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinConsumerArityJava.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityBean.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityComponent.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinFunctionArityJava.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityBean.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/arity/KotlinSupplierArityJava.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinConsumerArityBeanTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinFunctionArityBeanTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/web/KotlinSupplierArityBeanTest.kt 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")) + } +} From a8b0f891f7668448d3229c5f8a68ff653a50c490 Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 8 May 2025 15:17:31 +0200 Subject: [PATCH 2/6] Feat: Enhance Kotlin arity handling in function context This commit refactors the `spring-cloud-function-context` to comprehensively handle all combinations of possible declarations for Kotlin functions, consumers, and suppliers. This improves the framework's ability to correctly interpret and manage different Kotlin functional styles. Signed-off-by: Adrien Poupard --- .../context/FunctionRegistration.java | 13 +- .../BeanFactoryAwareFunctionRegistry.java | 26 +- .../context/catalog/FunctionTypeUtils.java | 5 + ...tlinLambdaToFunctionAutoConfiguration.java | 247 ------------------ .../config/KotlinLambdaToFunctionFactory.java | 116 ++++++++ .../wrapper/KotlinConsumerFlowWrapper.java | 98 +++++++ .../wrapper/KotlinConsumerPlainWrapper.java | 95 +++++++ .../KotlinConsumerSuspendFlowWrapper.java | 85 ++++++ .../KotlinConsumerSuspendPlainWrapper.java | 83 ++++++ .../KotlinFunctionFlowToFlowWrapper.java | 102 ++++++++ .../KotlinFunctionFlowToPlainWrapper.java | 96 +++++++ .../KotlinFunctionPlainToFlowWrapper.java | 98 +++++++ .../KotlinFunctionPlainToPlainWrapper.java | 88 +++++++ ...otlinFunctionSuspendFlowToFlowWrapper.java | 90 +++++++ ...tlinFunctionSuspendFlowToPlainWrapper.java | 90 +++++++ ...tlinFunctionSuspendPlainToFlowWrapper.java | 88 +++++++ ...linFunctionSuspendPlainToPlainWrapper.java | 86 ++++++ .../wrapper/KotlinFunctionWrapper.java | 30 +++ .../wrapper/KotlinSupplierFlowWrapper.java | 100 +++++++ .../wrapper/KotlinSupplierPlainWrapper.java | 96 +++++++ .../wrapper/KotlinSupplierSuspendWrapper.java | 91 +++++++ .../cloud/function/utils/KotlinUtils.java | 18 +- .../context/config/CoroutinesUtils.kt | 163 ++++++------ .../function/context/config/FunctionUtils.kt | 67 +++++ .../function/context/config/TypeUtils.kt | 107 ++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 - 26 files changed, 1830 insertions(+), 349 deletions(-) delete mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionFactory.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerFlowWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerPlainWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendFlowWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinConsumerSuspendPlainWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToFlowWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToPlainWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToFlowWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionPlainToPlainWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToFlowWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToPlainWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToFlowWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendPlainToPlainWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierFlowWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierPlainWrapper.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinSupplierSuspendWrapper.java create mode 100644 spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/FunctionUtils.kt create mode 100644 spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/TypeUtils.kt 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 From 5c917a5844ccef6e91cec54e84af4db3dd962b35 Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 8 May 2025 15:18:06 +0200 Subject: [PATCH 3/6] Test: Add unit tests for Kotlin arity and wrapper functionalities This commit adds new unit tests to verify the core logic for Kotlin arity handling and wrapper mechanisms. These tests cover various scenarios including catalogue registration and native type casting for consumers, functions, and suppliers. Signed-off-by: Adrien Poupard --- .../kotlin/ConsumerArityCatalogueTests.java | 281 ++++++++++ .../kotlin/FunctionArityCatalogueTests.java | 513 ++++++++++++++++++ .../KotlinConsumerArityNativeCastTests.java | 366 +++++++++++++ .../KotlinFunctionArityNativeCastTests.java | 443 +++++++++++++++ .../KotlinSupplierArityNativeCastTests.java | 379 +++++++++++++ .../kotlin/SupplierArityCatalogueTests.java | 354 ++++++++++++ .../wrapper/KotlinConsumerFlowWrapperTest.kt | 100 ++++ .../wrapper/KotlinConsumerPlainWrapperTest.kt | 95 ++++ .../KotlinConsumerSuspendFlowWrapperTest.kt | 130 +++++ .../KotlinConsumerSuspendPlainWrapperTest.kt | 1 + .../KotlinFunctionFlowToFlowWrapperTest.kt | 1 + .../KotlinFunctionFlowToPlainWrapperTest.kt | 1 + .../KotlinFunctionPlainToFlowWrapperTest.kt | 1 + .../KotlinFunctionPlainToPlainWrapperTest.kt | 126 +++++ ...linFunctionSuspendFlowToFlowWrapperTest.kt | 1 + ...inFunctionSuspendFlowToPlainWrapperTest.kt | 1 + ...inFunctionSuspendPlainToFlowWrapperTest.kt | 1 + ...nFunctionSuspendPlainToPlainWrapperTest.kt | 1 + .../wrapper/KotlinFunctionWrapperTest.kt | 72 +++ .../wrapper/KotlinSupplierFlowWrapperTest.kt | 140 +++++ .../wrapper/KotlinSupplierPlainWrapperTest.kt | 113 ++++ .../KotlinSupplierSuspendWrapperTest.kt | 1 + 22 files changed, 3121 insertions(+) create mode 100644 spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ConsumerArityCatalogueTests.java create mode 100644 spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/FunctionArityCatalogueTests.java create mode 100644 spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinConsumerArityNativeCastTests.java create mode 100644 spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinFunctionArityNativeCastTests.java create mode 100644 spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinSupplierArityNativeCastTests.java create mode 100644 spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/SupplierArityCatalogueTests.java create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerFlowWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerPlainWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendFlowWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinConsumerSuspendPlainWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToFlowWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionFlowToPlainWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToFlowWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionPlainToPlainWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToFlowWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendFlowToPlainWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToFlowWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionSuspendPlainToPlainWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinFunctionWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierFlowWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierPlainWrapperTest.kt create mode 100644 spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/wrapper/KotlinSupplierSuspendWrapperTest.kt 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/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 @@ + From 76537c10a03f719d291a1945c1070677bd86edd7 Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 8 May 2025 15:45:14 +0200 Subject: [PATCH 4/6] Docs: Update programming model with enhanced Kotlin support details Signed-off-by: Adrien Poupard --- .../programming-model.adoc | 170 ++++++++++++++++-- 1 file changed, 158 insertions(+), 12 deletions(-) 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 From 6dfc8ee6acfb9ac45f51921e766fb329dc2ea113 Mon Sep 17 00:00:00 2001 From: Adrien Poupard Date: Thu, 5 Jun 2025 14:01:46 +0200 Subject: [PATCH 5/6] Migrate kotlin `TypeUtils` methods to java `KotlinUtils` for improved Kotlin type handling and consistency. Signed-off-by: Adrien Poupard --- .../wrapper/KotlinConsumerFlowWrapper.java | 6 +- .../wrapper/KotlinConsumerPlainWrapper.java | 4 +- .../KotlinConsumerSuspendFlowWrapper.java | 6 +- .../KotlinConsumerSuspendPlainWrapper.java | 6 +- .../KotlinFunctionFlowToFlowWrapper.java | 10 +- .../KotlinFunctionFlowToPlainWrapper.java | 6 +- .../KotlinFunctionPlainToFlowWrapper.java | 8 +- ...otlinFunctionSuspendFlowToFlowWrapper.java | 10 +- ...tlinFunctionSuspendFlowToPlainWrapper.java | 12 +- ...tlinFunctionSuspendPlainToFlowWrapper.java | 8 +- ...linFunctionSuspendPlainToPlainWrapper.java | 6 +- .../wrapper/KotlinSupplierFlowWrapper.java | 7 +- .../wrapper/KotlinSupplierSuspendWrapper.java | 4 +- .../cloud/function/utils/KotlinUtils.java | 193 +++++++++++++++++- .../function/context/config/FunctionUtils.kt | 20 +- .../function/context/config/TypeUtils.kt | 107 ---------- 16 files changed, 248 insertions(+), 165 deletions(-) delete mode 100644 spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/TypeUtils.kt 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 index aed3799d8..eb4ab41e2 100644 --- 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 @@ -25,7 +25,7 @@ import reactor.core.publisher.Flux; import org.springframework.cloud.function.context.config.FunctionUtils; -import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -40,7 +40,7 @@ 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]); + return FunctionUtils.isValidKotlinConsumer(functionType, types) && KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, @@ -75,7 +75,7 @@ public String getName() { @Override public void accept(Flux props) { - Flow flow = TypeUtils.convertToFlow(props); + Flow flow = KotlinUtils.convertToFlow(props); invoke(flow); } 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 index 79be8a243..5f5893ec1 100644 --- 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 @@ -23,7 +23,7 @@ import kotlin.jvm.functions.Function1; import org.springframework.cloud.function.context.config.FunctionUtils; -import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -36,7 +36,7 @@ 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]); + return FunctionUtils.isValidKotlinConsumer(functionType, types) && !KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerPlainWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, 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 index 2060ed07e..197a471bb 100644 --- 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 @@ -25,7 +25,7 @@ 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.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -38,12 +38,12 @@ 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]); + return FunctionUtils.isValidKotlinSuspendConsumer(functionType, types) && KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerSuspendFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, Type[] propsTypes) { - ResolvableType continuationArgType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType continuationArgType = KotlinUtils.getSuspendingFunctionArgType(propsTypes[0]); ResolvableType functionType = ResolvableType.forClassWithGenerics(Consumer.class, ResolvableType.forClassWithGenerics(Flux.class, continuationArgType)); return new KotlinConsumerSuspendFlowWrapper(kotlinLambdaTarget, functionType, functionName); 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 index 63bd0e761..cbae98fef 100644 --- 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 @@ -24,7 +24,7 @@ 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.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -37,12 +37,12 @@ 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]); + return FunctionUtils.isValidKotlinSuspendConsumer(functionType, types) && !KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerSuspendPlainWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, Type[] propsTypes) { - ResolvableType continuationArgType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType continuationArgType = KotlinUtils.getSuspendingFunctionArgType(propsTypes[0]); ResolvableType functionType = ResolvableType.forClassWithGenerics(Consumer.class, continuationArgType); return new KotlinConsumerSuspendPlainWrapper(kotlinLambdaTarget, functionType, functionName); } 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 index 4ca47d9a4..c86397439 100644 --- 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 @@ -24,7 +24,7 @@ import reactor.core.publisher.Flux; import org.springframework.cloud.function.context.config.FunctionUtils; -import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -39,7 +39,7 @@ public final class KotlinFunctionFlowToFlowWrapper public static Boolean isValid(Type functionType, Type[] types) { return FunctionUtils.isValidKotlinFunction(functionType, types) && types.length == 2 - && TypeUtils.isFlowType(types[0]) && TypeUtils.isFlowType(types[1]); + && KotlinUtils.isFlowType(types[0]) && KotlinUtils.isFlowType(types[1]); } public static KotlinFunctionFlowToFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, @@ -73,16 +73,16 @@ public Flux apply(Flux input) { @Override public Flux invoke(Flux arg0) { - Flow flow = TypeUtils.convertToFlow(arg0); + Flow flow = KotlinUtils.convertToFlow(arg0); if (kotlinLambdaTarget instanceof Function1) { Function1, Flow> target = (Function1, Flow>) kotlinLambdaTarget; Flow result = target.invoke(flow); - return TypeUtils.convertToFlux(result); + return KotlinUtils.convertToFlux(result); } else if (kotlinLambdaTarget instanceof Function) { Function, Flow> target = (Function, Flow>) kotlinLambdaTarget; Flow result = target.apply(flow); - return TypeUtils.convertToFlux(result); + return KotlinUtils.convertToFlux(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/KotlinFunctionFlowToPlainWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionFlowToPlainWrapper.java index 84841feff..de6b828c4 100644 --- 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 @@ -24,7 +24,7 @@ import reactor.core.publisher.Flux; import org.springframework.cloud.function.context.config.FunctionUtils; -import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -39,7 +39,7 @@ public final class KotlinFunctionFlowToPlainWrapper public static Boolean isValid(Type functionType, Type[] types) { return FunctionUtils.isValidKotlinFunction(functionType, types) && types.length == 2 - && TypeUtils.isFlowType(types[0]) && !TypeUtils.isFlowType(types[1]); + && KotlinUtils.isFlowType(types[0]) && !KotlinUtils.isFlowType(types[1]); } public static KotlinFunctionFlowToPlainWrapper asRegistrationFunction(String functionName, @@ -64,7 +64,7 @@ public KotlinFunctionFlowToPlainWrapper(Object kotlinLambdaTarget, ResolvableTyp @Override public Object invoke(Flux arg0) { - Flow flow = TypeUtils.convertToFlow(arg0); + Flow flow = KotlinUtils.convertToFlow(arg0); if (kotlinLambdaTarget instanceof Function) { Function, Object> target = (Function, Object>) kotlinLambdaTarget; return target.apply(flow); 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 index 303601e16..2c2aeb318 100644 --- 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 @@ -24,7 +24,7 @@ import reactor.core.publisher.Flux; import org.springframework.cloud.function.context.config.FunctionUtils; -import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -40,7 +40,7 @@ public final class KotlinFunctionPlainToFlowWrapper public static Boolean isValid(Type functionType, Type[] types) { return FunctionUtils.isValidKotlinFunction(functionType, types) && types.length == 2 - && !TypeUtils.isFlowType(types[0]) && TypeUtils.isFlowType(types[1]); + && !KotlinUtils.isFlowType(types[0]) && KotlinUtils.isFlowType(types[1]); } public static KotlinFunctionPlainToFlowWrapper asRegistrationFunction(String functionName, @@ -68,12 +68,12 @@ public Flux invoke(Object arg0) { if (kotlinLambdaTarget instanceof Function) { Function> target = (Function>) kotlinLambdaTarget; Flow result = target.apply(arg0); - return TypeUtils.convertToFlux(result); + return KotlinUtils.convertToFlux(result); } else if (kotlinLambdaTarget instanceof Function1) { Function1> target = (Function1>) kotlinLambdaTarget; Flow result = target.invoke(arg0); - return TypeUtils.convertToFlux(result); + return KotlinUtils.convertToFlux(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/KotlinFunctionSuspendFlowToFlowWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/wrapper/KotlinFunctionSuspendFlowToFlowWrapper.java index f95838b2c..041f0d6f2 100644 --- 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 @@ -25,7 +25,7 @@ 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.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -41,13 +41,13 @@ public final class KotlinFunctionSuspendFlowToFlowWrapper public static Boolean isValid(Type functionType, Type[] types) { return FunctionUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 - && TypeUtils.isFlowType(types[0]) && TypeUtils.isContinuationFlowType(types[1]); + && KotlinUtils.isFlowType(types[0]) && KotlinUtils.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 argType = KotlinUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType returnType = KotlinUtils.getSuspendingFunctionReturnType(propsTypes[1]); ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, ResolvableType.forClassWithGenerics(Flux.class, argType), ResolvableType.forClassWithGenerics(Flux.class, returnType)); @@ -68,7 +68,7 @@ public KotlinFunctionSuspendFlowToFlowWrapper(Object kotlinLambdaTarget, Resolva @Override public Flux invoke(Flux arg0) { - Flow flow = TypeUtils.convertToFlow(arg0); + Flow flow = KotlinUtils.convertToFlow(arg0); return CoroutinesUtils.invokeSuspendingFlowFunction(kotlinLambdaTarget, flow); } 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 index 2128f8b08..d4ccbdcfd 100644 --- 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 @@ -25,7 +25,7 @@ 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.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -40,14 +40,14 @@ public final class KotlinFunctionSuspendFlowToPlainWrapper 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]); + && KotlinUtils.isFlowType(types[0]) && KotlinUtils.isContinuationType(types[1]) + && !KotlinUtils.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 argType = KotlinUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType result = KotlinUtils.getSuspendingFunctionReturnType(propsTypes[1]); ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, ResolvableType.forClassWithGenerics(Flux.class, argType), result); return new KotlinFunctionSuspendFlowToPlainWrapper(kotlinLambdaTarget, functionType, functionName); @@ -73,7 +73,7 @@ public ResolvableType getResolvableType() { @Override public Object invoke(Flux arg0) { - Flow flow = TypeUtils.convertToFlow(arg0); + Flow flow = KotlinUtils.convertToFlow(arg0); return CoroutinesUtils.invokeSuspendingFlowFunction(kotlinLambdaTarget, flow); } 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 index 9245cd3af..7bebda321 100644 --- 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 @@ -24,7 +24,7 @@ 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.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -40,13 +40,13 @@ public final class KotlinFunctionSuspendPlainToFlowWrapper public static Boolean isValid(Type functionType, Type[] types) { return FunctionUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 - && !TypeUtils.isFlowType(types[0]) && TypeUtils.isContinuationFlowType(types[1]); + && !KotlinUtils.isFlowType(types[0]) && KotlinUtils.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 argType = KotlinUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType returnType = KotlinUtils.getSuspendingFunctionReturnType(propsTypes[1]); ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, argType, ResolvableType.forClassWithGenerics(Flux.class, returnType)); return new KotlinFunctionSuspendPlainToFlowWrapper(kotlinLambdaTarget, functionType, functionName); 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 index 274adf1ee..1db57971b 100644 --- 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 @@ -24,7 +24,7 @@ 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.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -43,8 +43,8 @@ public static Boolean isValid(Type functionType, Type[] types) { public static KotlinFunctionSuspendPlainToPlainWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, Type[] propsTypes) { - ResolvableType argType = TypeUtils.getSuspendingFunctionArgType(propsTypes[0]); - ResolvableType returnType = TypeUtils.getSuspendingFunctionReturnType(propsTypes[1]); + ResolvableType argType = KotlinUtils.getSuspendingFunctionArgType(propsTypes[0]); + ResolvableType returnType = KotlinUtils.getSuspendingFunctionReturnType(propsTypes[1]); ResolvableType functionType = ResolvableType.forClassWithGenerics(Function.class, argType, ResolvableType.forClassWithGenerics(Flux.class, returnType)); return new KotlinFunctionSuspendPlainToPlainWrapper(kotlinLambdaTarget, functionType, functionName); 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 index 2ccc2be30..036405823 100644 --- 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 @@ -24,10 +24,9 @@ import reactor.core.publisher.Flux; import org.springframework.cloud.function.context.config.FunctionUtils; -import org.springframework.cloud.function.context.config.TypeUtils; +import org.springframework.cloud.function.utils.KotlinUtils; 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 @@ -41,7 +40,7 @@ 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]); + return FunctionUtils.isValidKotlinSupplier(functionType) && types.length == 1 && KotlinUtils.isFlowType(types[0]); } public static KotlinSupplierFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, @@ -78,7 +77,7 @@ public String getName() { @Override public Flux get() { Flow result = invoke(); - return convertToFlux(result); + return KotlinUtils.convertToFlux(result); } @Override 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 index 7a42282fb..370f9531d 100644 --- 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 @@ -24,7 +24,7 @@ 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.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; import org.springframework.util.ObjectUtils; @@ -43,7 +43,7 @@ public static Boolean isValid(Type functionType, Type[] types) { public static KotlinSupplierSuspendWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, Type[] propsTypes) { - ResolvableType returnType = TypeUtils.getSuspendingFunctionReturnType(propsTypes[0]); + ResolvableType returnType = KotlinUtils.getSuspendingFunctionReturnType(propsTypes[0]); ResolvableType functionType = ResolvableType.forClassWithGenerics(Supplier.class, ResolvableType.forClassWithGenerics(Flux.class, returnType)); return new KotlinSupplierSuspendWrapper(kotlinLambdaTarget, functionType, functionName); 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 0901fb9b7..76d0a0465 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 @@ -18,19 +18,28 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import kotlin.coroutines.Continuation; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.reactive.ReactiveFlowKt; +import kotlinx.coroutines.reactor.ReactorFlowKt; +import reactor.core.publisher.Flux; -import org.springframework.cloud.function.context.config.TypeUtils; import org.springframework.core.KotlinDetector; +import org.springframework.core.ResolvableType; /** + * Utility methods for working with Kotlin types. + * * @author Oleg Zhurakousky + * @author Adrien Poupard */ public final class KotlinUtils { private KotlinUtils() { - + // Utility class should not be instantiated } public static boolean isKotlinType(Object object, Type functionType) { @@ -44,10 +53,188 @@ public static boolean isKotlinType(Object object, Type functionType) { // 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 hasFlowType(types); } return false; } return false; } + + /** + * Get the argument type of a suspending function. + * + * @param type the function type + * @return the resolved argument type + */ + public static ResolvableType getSuspendingFunctionArgType(Type type) { + return ResolvableType.forType(getFlowTypeArguments(type)); + } + + /** + * Get the return type of a suspending function. + * + * @param type the function type + * @return the resolved return type + */ + public static ResolvableType getSuspendingFunctionReturnType(Type type) { + Type lower = getContinuationTypeArguments(type); + return ResolvableType.forType(getFlowTypeArguments(lower)); + } + + /** + * Get the type arguments of a Flow type. + * + * @param type the Flow type + * @return the type arguments + */ + public static Type getFlowTypeArguments(Type type) { + if (!isFlowType(type)) { + return type; + } + ParameterizedType parameterizedLowerType = (ParameterizedType) type; + if (parameterizedLowerType.getActualTypeArguments().length == 0) { + return parameterizedLowerType; + } + + Type actualTypeArgument = parameterizedLowerType.getActualTypeArguments()[0]; + if (actualTypeArgument instanceof WildcardType) { + WildcardType wildcardTypeLower = (WildcardType) parameterizedLowerType.getActualTypeArguments()[0]; + return wildcardTypeLower.getUpperBounds()[0]; + } + else { + return actualTypeArgument; + } + } + + /** + * Check if any of the types is a Flow type. + * + * @param types the types to check + * @return true if any type is a Flow type + */ + public static boolean hasFlowType(Type[] types) { + for (Type type : types) { + if (isFlowType(type)) { + return true; + } + } + return false; + } + + /** + * Check if the type is a Flow type. + * + * @param type the type to check + * @return true if the type is a Flow type + */ + public static boolean isFlowType(Type type) { + return type.getTypeName().startsWith(Flow.class.getName()); + } + + /** + * Check if the type is a Continuation type. + * + * @param type the type to check + * @return true if the type is a Continuation type + */ + public static boolean isContinuationType(Type type) { + return type.getTypeName().startsWith(Continuation.class.getName()); + } + + /** + * Check if the type is a Unit type. + * + * @param type the type to check + * @return true if the type is a Unit type + */ + public static boolean isUnitType(Type type) { + return isTypeRepresentedByClass(type, kotlin.Unit.class); + } + + /** + * Check if the type is a Void type. + * + * @param type the type to check + * @return true if the type is a Void type + */ + public static boolean isVoidType(Type type) { + return isTypeRepresentedByClass(type, Void.class); + } + + /** + * Check if the type is a Continuation of Unit type. + * + * @param type the type to check + * @return true if the type is a Continuation of Unit type + */ + public static boolean isContinuationUnitType(Type type) { + return isContinuationType(type) && type.getTypeName().contains(kotlin.Unit.class.getName()); + } + + /** + * Check if the type is a Continuation of Flow type. + * + * @param type the type to check + * @return true if the type is a Continuation of Flow type + */ + public static boolean isContinuationFlowType(Type type) { + return isContinuationType(type) && type.getTypeName().contains(Flow.class.getName()); + } + + /** + * Get the type arguments of a Continuation type. + * + * @param type the Continuation type + * @return the type arguments + */ + public static Type getContinuationTypeArguments(Type type) { + if (!isContinuationType(type)) { + return type; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + Type typeArg = parameterizedType.getActualTypeArguments()[0]; + + if (typeArg instanceof WildcardType) { + return ((WildcardType) typeArg).getLowerBounds()[0]; + } + else if (typeArg instanceof ParameterizedType) { + return typeArg; + } + else { + return typeArg; + } + } + + /** + * Convert a Flux to a Flow. + * + * @param arg0 the Flux to convert + * @param the element type + * @return the converted Flow + */ + public static Flow convertToFlow(Flux arg0) { + return ReactiveFlowKt.asFlow(arg0); + } + + /** + * Convert a Flow to a Flux. + * + * @param arg0 the Flow to convert + * @param the element type + * @return the converted Flux + */ + public static Flux convertToFlux(Flow arg0) { + return ReactorFlowKt.asFlux(arg0); + } + + /** + * Check if a type is represented by a specific class. + * + * @param type the type to check + * @param clazz the class to check against + * @return true if the type is represented by the class + */ + private static boolean isTypeRepresentedByClass(Type type, Class clazz) { + return type.getTypeName().contains(clazz.getName()); + } } 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 index a449f1585..b82f61156 100644 --- 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 @@ -22,34 +22,38 @@ import java.lang.reflect.Type import java.util.function.Consumer import java.util.function.Function import java.util.function.Supplier +import kotlin.jvm.functions.Function0 +import kotlin.jvm.functions.Function1 +import kotlin.jvm.functions.Function2 +import org.springframework.cloud.function.utils.KotlinUtils 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])) + !KotlinUtils.isContinuationType(type[0]) && + (KotlinUtils.isUnitType(type[1]) || KotlinUtils.isVoidType(type[1])) ) } fun isValidKotlinSuspendConsumer(functionType: Type, type: Array): Boolean { return isTypeRepresentedByClass(functionType, Function2::class.java) && type.size == 3 && - isContinuationUnitType(type[1]) + KotlinUtils.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]) + type.size == 2 && !KotlinUtils.isContinuationType(type[0]) && + !KotlinUtils.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]) + KotlinUtils.isContinuationType(type[1]) && + !KotlinUtils.isContinuationUnitType(type[1]) } fun isValidKotlinSupplier(functionType: Type): Boolean { @@ -59,7 +63,7 @@ fun isValidKotlinSupplier(functionType: Type): Boolean { fun isValidKotlinSuspendSupplier(functionType: Type, type: Array): Boolean { return isTypeRepresentedByClass(functionType, Function1::class.java) && type.size == 2 && - isContinuationType(type[0]) + KotlinUtils.isContinuationType(type[0]) } fun isTypeRepresentedByClass(type: Type, clazz: Class<*>): Boolean { 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 deleted file mode 100644 index 42d44bb2b..000000000 --- a/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/TypeUtils.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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() -} From 50b8929fb6a6dd2f2ad059a2b04aadc59bd4b8f3 Mon Sep 17 00:00:00 2001 From: Adrien Poupard Date: Thu, 5 Jun 2025 19:46:06 +0200 Subject: [PATCH 6/6] Refactor: Replace FunctionUtils with KotlinUtils for consumer and function validation Signed-off-by: Adrien Poupard --- .../wrapper/KotlinConsumerFlowWrapper.java | 3 +- .../wrapper/KotlinConsumerPlainWrapper.java | 3 +- .../KotlinConsumerSuspendFlowWrapper.java | 3 +- .../KotlinConsumerSuspendPlainWrapper.java | 3 +- .../KotlinFunctionFlowToFlowWrapper.java | 3 +- .../KotlinFunctionFlowToPlainWrapper.java | 3 +- .../KotlinFunctionPlainToFlowWrapper.java | 3 +- .../KotlinFunctionPlainToPlainWrapper.java | 4 +- ...otlinFunctionSuspendFlowToFlowWrapper.java | 3 +- ...tlinFunctionSuspendFlowToPlainWrapper.java | 3 +- ...tlinFunctionSuspendPlainToFlowWrapper.java | 3 +- ...linFunctionSuspendPlainToPlainWrapper.java | 3 +- .../wrapper/KotlinSupplierFlowWrapper.java | 3 +- .../wrapper/KotlinSupplierPlainWrapper.java | 4 +- .../wrapper/KotlinSupplierSuspendWrapper.java | 3 +- .../cloud/function/utils/KotlinUtils.java | 84 ++++++++++++++++++- .../function/context/config/FunctionUtils.kt | 71 ---------------- 17 files changed, 100 insertions(+), 102 deletions(-) delete mode 100644 spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/FunctionUtils.kt 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 index eb4ab41e2..c219a8e14 100644 --- 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 @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.Flow; import reactor.core.publisher.Flux; -import org.springframework.cloud.function.context.config.FunctionUtils; import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -40,7 +39,7 @@ public final class KotlinConsumerFlowWrapper implements KotlinFunctionWrapper, Consumer>, Function1, Unit> { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinConsumer(functionType, types) && KotlinUtils.isFlowType(types[0]); + return KotlinUtils.isValidKotlinConsumer(functionType, types) && KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, 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 index 5f5893ec1..1c0032b39 100644 --- 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 @@ -22,7 +22,6 @@ import kotlin.Unit; import kotlin.jvm.functions.Function1; -import org.springframework.cloud.function.context.config.FunctionUtils; import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -36,7 +35,7 @@ public final class KotlinConsumerPlainWrapper implements KotlinFunctionWrapper, Consumer, Function1 { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinConsumer(functionType, types) && !KotlinUtils.isFlowType(types[0]); + return KotlinUtils.isValidKotlinConsumer(functionType, types) && !KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerPlainWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, 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 index 197a471bb..27ad3a677 100644 --- 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 @@ -24,7 +24,6 @@ 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.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -38,7 +37,7 @@ public final class KotlinConsumerSuspendFlowWrapper implements KotlinFunctionWrapper, Consumer>, Function1, Unit> { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinSuspendConsumer(functionType, types) && KotlinUtils.isFlowType(types[0]); + return KotlinUtils.isValidKotlinSuspendConsumer(functionType, types) && KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerSuspendFlowWrapper asRegistrationFunction(String functionName, 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 index cbae98fef..e8d9df246 100644 --- 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 @@ -23,7 +23,6 @@ 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.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -37,7 +36,7 @@ public final class KotlinConsumerSuspendPlainWrapper implements KotlinFunctionWrapper, Consumer, Function1 { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinSuspendConsumer(functionType, types) && !KotlinUtils.isFlowType(types[0]); + return KotlinUtils.isValidKotlinSuspendConsumer(functionType, types) && !KotlinUtils.isFlowType(types[0]); } public static KotlinConsumerSuspendPlainWrapper asRegistrationFunction(String functionName, 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 index c86397439..cf47ef0de 100644 --- 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 @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.Flow; import reactor.core.publisher.Flux; -import org.springframework.cloud.function.context.config.FunctionUtils; import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -38,7 +37,7 @@ 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 + return KotlinUtils.isValidKotlinFunction(functionType, types) && types.length == 2 && KotlinUtils.isFlowType(types[0]) && KotlinUtils.isFlowType(types[1]); } 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 index de6b828c4..eebf86a53 100644 --- 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 @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.Flow; import reactor.core.publisher.Flux; -import org.springframework.cloud.function.context.config.FunctionUtils; import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -38,7 +37,7 @@ 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 + return KotlinUtils.isValidKotlinFunction(functionType, types) && types.length == 2 && KotlinUtils.isFlowType(types[0]) && !KotlinUtils.isFlowType(types[1]); } 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 index 2c2aeb318..816bfa801 100644 --- 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 @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.Flow; import reactor.core.publisher.Flux; -import org.springframework.cloud.function.context.config.FunctionUtils; import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -39,7 +38,7 @@ public final class KotlinFunctionPlainToFlowWrapper implements KotlinFunctionWrapper, Function>, Function1> { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinFunction(functionType, types) && types.length == 2 + return KotlinUtils.isValidKotlinFunction(functionType, types) && types.length == 2 && !KotlinUtils.isFlowType(types[0]) && KotlinUtils.isFlowType(types[1]); } 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 index 29f22460c..c101ff2df 100644 --- 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 @@ -21,7 +21,7 @@ import kotlin.jvm.functions.Function1; -import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; /** @@ -35,7 +35,7 @@ public final class KotlinFunctionPlainToPlainWrapper implements KotlinFunctionWrapper, Function, Function1 { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinFunction(functionType, types); + return KotlinUtils.isValidKotlinFunction(functionType, types); } public static KotlinFunctionPlainToPlainWrapper asRegistrationFunction(String functionName, 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 index 041f0d6f2..8bca5972b 100644 --- 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 @@ -24,7 +24,6 @@ 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.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -40,7 +39,7 @@ 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 + return KotlinUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 && KotlinUtils.isFlowType(types[0]) && KotlinUtils.isContinuationFlowType(types[1]); } 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 index d4ccbdcfd..b0261c3e1 100644 --- 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 @@ -24,7 +24,6 @@ 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.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -39,7 +38,7 @@ 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 + return KotlinUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 && KotlinUtils.isFlowType(types[0]) && KotlinUtils.isContinuationType(types[1]) && !KotlinUtils.isContinuationFlowType(types[1]); } 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 index 7bebda321..59361918f 100644 --- 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 @@ -23,7 +23,6 @@ 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.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -39,7 +38,7 @@ public final class KotlinFunctionSuspendPlainToFlowWrapper implements KotlinFunctionWrapper, Function>, Function1> { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 + return KotlinUtils.isValidKotlinSuspendFunction(functionType, types) && types.length == 3 && !KotlinUtils.isFlowType(types[0]) && KotlinUtils.isContinuationFlowType(types[1]); } 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 index 1db57971b..6dc7b9cdd 100644 --- 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 @@ -23,7 +23,6 @@ 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.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -38,7 +37,7 @@ public final class KotlinFunctionSuspendPlainToPlainWrapper implements KotlinFunctionWrapper, Function, Function1 { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinSuspendFunction(functionType, types); + return KotlinUtils.isValidKotlinSuspendFunction(functionType, types); } public static KotlinFunctionSuspendPlainToPlainWrapper asRegistrationFunction(String functionName, 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 index 036405823..131fa94b2 100644 --- 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 @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.Flow; import reactor.core.publisher.Flux; -import org.springframework.cloud.function.context.config.FunctionUtils; import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; @@ -40,7 +39,7 @@ public final class KotlinSupplierFlowWrapper implements KotlinFunctionWrapper, Supplier>, Function0> { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinSupplier(functionType) && types.length == 1 && KotlinUtils.isFlowType(types[0]); + return KotlinUtils.isValidKotlinSupplier(functionType) && types.length == 1 && KotlinUtils.isFlowType(types[0]); } public static KotlinSupplierFlowWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, 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 index 985430770..4eaaec082 100644 --- 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 @@ -21,7 +21,7 @@ import kotlin.jvm.functions.Function0; -import org.springframework.cloud.function.context.config.FunctionUtils; +import org.springframework.cloud.function.utils.KotlinUtils; import org.springframework.core.ResolvableType; import org.springframework.util.ObjectUtils; @@ -36,7 +36,7 @@ public final class KotlinSupplierPlainWrapper implements KotlinFunctionWrapper, Supplier, Function0 { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinSupplier(functionType); + return KotlinUtils.isValidKotlinSupplier(functionType); } public static KotlinSupplierPlainWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, 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 index 370f9531d..76aa217c0 100644 --- 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 @@ -23,7 +23,6 @@ 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.utils.KotlinUtils; import org.springframework.core.ResolvableType; import org.springframework.util.ObjectUtils; @@ -38,7 +37,7 @@ public final class KotlinSupplierSuspendWrapper implements KotlinFunctionWrapper, Supplier, Function0 { public static Boolean isValid(Type functionType, Type[] types) { - return FunctionUtils.isValidKotlinSuspendSupplier(functionType, types); + return KotlinUtils.isValidKotlinSuspendSupplier(functionType, types); } public static KotlinSupplierSuspendWrapper asRegistrationFunction(String functionName, Object kotlinLambdaTarget, 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 76d0a0465..397eabce7 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 @@ -19,10 +19,14 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; import kotlin.coroutines.Continuation; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; import kotlinx.coroutines.flow.Flow; import kotlinx.coroutines.reactive.ReactiveFlowKt; import kotlinx.coroutines.reactor.ReactorFlowKt; @@ -227,6 +231,84 @@ public static Flux convertToFlux(Flow arg0) { return ReactorFlowKt.asFlux(arg0); } + /** + * Check if a type is a valid Kotlin consumer. + * + * @param functionType the function type to check + * @param type the type arguments + * @return true if the type is a valid Kotlin consumer + */ + public static boolean isValidKotlinConsumer(Type functionType, Type[] type) { + return isTypeRepresentedByClass(functionType, Consumer.class) || ( + isTypeRepresentedByClass(functionType, Function1.class) && + type.length == 2 && + !isContinuationType(type[0]) && + (isUnitType(type[1]) || isVoidType(type[1])) + ); + } + + /** + * Check if a type is a valid Kotlin suspend consumer. + * + * @param functionType the function type to check + * @param type the type arguments + * @return true if the type is a valid Kotlin suspend consumer + */ + public static boolean isValidKotlinSuspendConsumer(Type functionType, Type[] type) { + return isTypeRepresentedByClass(functionType, Function2.class) && type.length == 3 && + isContinuationUnitType(type[1]); + } + + /** + * Check if a type is a valid Kotlin function. + * + * @param functionType the function type to check + * @param type the type arguments + * @return true if the type is a valid Kotlin function + */ + public static boolean isValidKotlinFunction(Type functionType, Type[] type) { + return (isTypeRepresentedByClass(functionType, Function1.class) || + isTypeRepresentedByClass(functionType, Function.class)) && + type.length == 2 && !isContinuationType(type[0]) && + !isUnitType(type[1]); + } + + /** + * Check if a type is a valid Kotlin suspend function. + * + * @param functionType the function type to check + * @param type the type arguments + * @return true if the type is a valid Kotlin suspend function + */ + public static boolean isValidKotlinSuspendFunction(Type functionType, Type[] type) { + return isTypeRepresentedByClass(functionType, Function2.class) && type.length == 3 && + isContinuationType(type[1]) && + !isContinuationUnitType(type[1]); + } + + /** + * Check if a type is a valid Kotlin supplier. + * + * @param functionType the function type to check + * @return true if the type is a valid Kotlin supplier + */ + public static boolean isValidKotlinSupplier(Type functionType) { + return isTypeRepresentedByClass(functionType, Function0.class) || + isTypeRepresentedByClass(functionType, Supplier.class); + } + + /** + * Check if a type is a valid Kotlin suspend supplier. + * + * @param functionType the function type to check + * @param type the type arguments + * @return true if the type is a valid Kotlin suspend supplier + */ + public static boolean isValidKotlinSuspendSupplier(Type functionType, Type[] type) { + return isTypeRepresentedByClass(functionType, Function1.class) && type.length == 2 && + isContinuationType(type[0]); + } + /** * Check if a type is represented by a specific class. * @@ -234,7 +316,7 @@ public static Flux convertToFlux(Flow arg0) { * @param clazz the class to check against * @return true if the type is represented by the class */ - private static boolean isTypeRepresentedByClass(Type type, Class clazz) { + public static boolean isTypeRepresentedByClass(Type type, Class clazz) { return type.getTypeName().contains(clazz.getName()); } } 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 deleted file mode 100644 index b82f61156..000000000 --- a/spring-cloud-function-context/src/main/kotlin/org/springframework/cloud/function/context/config/FunctionUtils.kt +++ /dev/null @@ -1,71 +0,0 @@ - -/* - * 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 -import kotlin.jvm.functions.Function0 -import kotlin.jvm.functions.Function1 -import kotlin.jvm.functions.Function2 -import org.springframework.cloud.function.utils.KotlinUtils - -fun isValidKotlinConsumer(functionType: Type, type: Array): Boolean { - return isTypeRepresentedByClass(functionType, Consumer::class.java) || ( - isTypeRepresentedByClass(functionType, Function1::class.java) && - type.size == 2 && - !KotlinUtils.isContinuationType(type[0]) && - (KotlinUtils.isUnitType(type[1]) || KotlinUtils.isVoidType(type[1])) - ) -} - -fun isValidKotlinSuspendConsumer(functionType: Type, type: Array): Boolean { - return isTypeRepresentedByClass(functionType, Function2::class.java) && type.size == 3 && - KotlinUtils.isContinuationUnitType(type[1]) -} - - -fun isValidKotlinFunction(functionType: Type, type: Array): Boolean { - return (isTypeRepresentedByClass(functionType, Function1::class.java) || - isTypeRepresentedByClass(functionType, Function::class.java)) && - type.size == 2 && !KotlinUtils.isContinuationType(type[0]) && - !KotlinUtils.isUnitType(type[1]) -} - - -fun isValidKotlinSuspendFunction(functionType: Type, type: Array): Boolean { - return isTypeRepresentedByClass(functionType, Function2::class.java) && type.size == 3 && - KotlinUtils.isContinuationType(type[1]) && - !KotlinUtils.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 && - KotlinUtils.isContinuationType(type[0]) -} - -fun isTypeRepresentedByClass(type: Type, clazz: Class<*>): Boolean { - return type.typeName.contains(clazz.name) -}