Add Kotlin suspend function support
Use suspendCoroutineUninterceptedOrReturn to avoid using not fully implemented Function2.reflect() Mapping of Function, Consumer and Supplier to kotlin suspend flow lambda Fix MR review Resolves #655
This commit is contained in:
committed by
Oleg Zhurakousky
parent
3c16efc6a4
commit
e30a091f82
@@ -24,11 +24,34 @@
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-coroutines-reactor</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webflux</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
||||
@@ -22,7 +22,6 @@ 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;
|
||||
@@ -30,6 +29,7 @@ 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;
|
||||
@@ -50,11 +50,13 @@ 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
|
||||
* @since 2.0
|
||||
*/
|
||||
@Configuration
|
||||
@@ -147,11 +149,17 @@ public class KotlinLambdaToFunctionAutoConfiguration {
|
||||
|
||||
@Override
|
||||
public Object invoke(Object arg0) {
|
||||
if (CoroutinesUtils.isValidSuspendingFunction(kotlinLambdaTarget, arg0)) {
|
||||
return CoroutinesUtils.invokeSuspendingFunction(kotlinLambdaTarget, arg0);
|
||||
}
|
||||
return ((Function1) this.kotlinLambdaTarget).invoke(arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke() {
|
||||
if (CoroutinesUtils.isValidSuspendingSupplier(kotlinLambdaTarget)) {
|
||||
return CoroutinesUtils.invokeSuspendingSupplier(kotlinLambdaTarget);
|
||||
}
|
||||
return ((Function0) this.kotlinLambdaTarget).invoke();
|
||||
}
|
||||
|
||||
@@ -168,19 +176,43 @@ public class KotlinLambdaToFunctionAutoConfiguration {
|
||||
@Override
|
||||
public FunctionRegistration getObject() throws Exception {
|
||||
String name = this.name.endsWith(FunctionRegistration.REGISTRATION_NAME_SUFFIX)
|
||||
? this.name.replace(FunctionRegistration.REGISTRATION_NAME_SUFFIX, "")
|
||||
: this.name;
|
||||
? 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 (functionType.getTypeName().contains("Function0")) {
|
||||
functionType = ResolvableType.forClassWithGenerics(Supplier.class, ResolvableType.forType(types[0]))
|
||||
.getType();
|
||||
.getType();
|
||||
|
||||
}
|
||||
else if (functionType.getTypeName().contains("Function1")) {
|
||||
else if (isValidKotlinFunction(functionType, types)) {
|
||||
functionType = ResolvableType.forClassWithGenerics(Function.class, ResolvableType.forType(types[0]),
|
||||
ResolvableType.forType(types[1])).getType();
|
||||
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 {
|
||||
throw new UnsupportedOperationException("Multi argument Kotlin functions are not currently supported");
|
||||
@@ -189,6 +221,22 @@ public class KotlinLambdaToFunctionAutoConfiguration {
|
||||
return registration;
|
||||
}
|
||||
|
||||
private boolean isValidKotlinFunction(Type functionType, Type[] type) {
|
||||
return functionType.getTypeName().contains(Function1.class.getName()) && type.length == 2 && !CoroutinesUtils.isContinuationType(type[0]);
|
||||
}
|
||||
|
||||
private boolean isValidKotlinSuspendSupplier(Type functionType, Type[] type) {
|
||||
return functionType.getTypeName().contains(Function1.class.getName()) && type.length == 2 && CoroutinesUtils.isContinuationFlowType(type[0]);
|
||||
}
|
||||
|
||||
private boolean isValidKotlinSuspendConsumer(Type functionType, Type[] type) {
|
||||
return functionType.getTypeName().contains(Function2.class.getName()) && type.length == 3 && CoroutinesUtils.isFlowType(type[0]) && CoroutinesUtils.isContinuationUnitType(type[1]);
|
||||
}
|
||||
|
||||
private boolean isValidKotlinSuspendFunction(Type functionType, Type[] type) {
|
||||
return functionType.getTypeName().contains(Function2.class.getName()) && type.length == 3 && CoroutinesUtils.isContinuationFlowType(type[1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return FunctionRegistration.class;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:JvmName("CoroutinesUtils")
|
||||
package org.springframework.cloud.function.context.config
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import kotlinx.coroutines.reactor.asFlux
|
||||
import kotlinx.coroutines.reactor.mono
|
||||
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
|
||||
|
||||
fun isValidSuspendingFunction(kotlinLambdaTarget: Any, arg0: Any): Boolean {
|
||||
return arg0 is Flux<*> && kotlinLambdaTarget is Function2<*, *, *>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
val parameterizedType = type as ParameterizedType
|
||||
val wildcardType = parameterizedType.actualTypeArguments[0] as WildcardType
|
||||
return wildcardType.lowerBounds[0]
|
||||
}
|
||||
|
||||
fun invokeSuspendingFunction(kotlinLambdaTarget: Any, arg0: Any): Flux<Any> {
|
||||
val function = kotlinLambdaTarget as SuspendFunction
|
||||
val flux = arg0 as Flux<Any>
|
||||
return fluxSuspendingFlowFunction(flux, function)
|
||||
}
|
||||
|
||||
fun isValidSuspendingSupplier(kotlinLambdaTarget: Any): Boolean {
|
||||
return kotlinLambdaTarget is Function1<*, *>
|
||||
}
|
||||
|
||||
fun invokeSuspendingSupplier(kotlinLambdaTarget: Any): Flux<Any> {
|
||||
val supplier = kotlinLambdaTarget as SuspendSupplier
|
||||
return mono(Dispatchers.Unconfined) {
|
||||
suspendCoroutineUninterceptedOrReturn<Flow<Any>> {
|
||||
supplier.invoke(it)
|
||||
}
|
||||
}.flatMapMany {
|
||||
it.asFlux()
|
||||
}
|
||||
}
|
||||
|
||||
fun fluxSuspendingFlowFunction(flux: Flux<Any>, target: SuspendFunction): Flux<Any> {
|
||||
return mono(Dispatchers.Unconfined) {
|
||||
suspendCoroutineUninterceptedOrReturn<Flow<Any>> {
|
||||
target.invoke(flux.asFlow(), it)
|
||||
}
|
||||
}.flatMapMany {
|
||||
it.asFlux()
|
||||
}
|
||||
}
|
||||
|
||||
private typealias SuspendFunction = (Any?, Any?) -> Any?
|
||||
private typealias SuspendSupplier = (Any?) -> Any?
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 java.lang.reflect.ParameterizedType;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import kotlin.jvm.functions.Function2;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* @author Adrien Poupard
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class ContextFunctionCatalogAutoConfigurationKotlinSuspendTests {
|
||||
|
||||
private GenericApplicationContext context;
|
||||
|
||||
private FunctionCatalog catalog;
|
||||
|
||||
@AfterEach
|
||||
public void close() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeDiscoveryTests() {
|
||||
create(new Class[] { KotlinSuspendFlowLambdasConfiguration.class,
|
||||
ContextFunctionCatalogAutoConfigurationKotlinTests.SimpleConfiguration.class });
|
||||
|
||||
Object function = this.context.getBean("kotlinFunction");
|
||||
ParameterizedType functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinFunction", this.context);
|
||||
assertThat(functionType.getRawType().getTypeName()).isEqualTo(Function.class.getName());
|
||||
assertThat(functionType.getActualTypeArguments().length).isEqualTo(2);
|
||||
assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux<java.lang.String>");
|
||||
assertThat(functionType.getActualTypeArguments()[1].getTypeName()).isEqualTo("reactor.core.publisher.Flux<java.lang.String>");
|
||||
|
||||
function = this.context.getBean("kotlinConsumer");
|
||||
functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinConsumer", this.context);
|
||||
assertThat(functionType.getRawType().getTypeName()).isEqualTo(Consumer.class.getName());
|
||||
assertThat(functionType.getActualTypeArguments().length).isEqualTo(1);
|
||||
assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux<java.lang.String>");
|
||||
|
||||
function = this.context.getBean("kotlinSupplier");
|
||||
functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinSupplier", this.context);
|
||||
assertThat(functionType.getRawType().getTypeName()).isEqualTo(Supplier.class.getName());
|
||||
assertThat(functionType.getActualTypeArguments().length).isEqualTo(1);
|
||||
assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux<java.lang.String>");
|
||||
|
||||
function = this.context.getBean("kotlinPojoFunction");
|
||||
functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinPojoFunction", this.context);
|
||||
assertThat(functionType.getRawType().getTypeName()).isEqualTo(Function.class.getName());
|
||||
assertThat(functionType.getActualTypeArguments().length).isEqualTo(2);
|
||||
assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux<org.springframework.cloud.function.kotlin.Person>");
|
||||
assertThat(functionType.getActualTypeArguments()[1].getTypeName()).isEqualTo("reactor.core.publisher.Flux<java.lang.String>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotLoadKotlinSuspendLambasNotUsingFlow() {
|
||||
create(new Class[] { KotlinSuspendLambdasConfiguration.class,
|
||||
ContextFunctionCatalogAutoConfigurationKotlinTests.SimpleConfiguration.class });
|
||||
|
||||
assertThat(this.context.getBean("kotlinFunction")).isInstanceOf(Function2.class);
|
||||
assertThatThrownBy(() -> {
|
||||
this.catalog.lookup(Function.class, "kotlinFunction");
|
||||
}).isInstanceOf(BeanCreationException.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
this.catalog.lookup(Function.class, "kotlinConsumer");
|
||||
}).isInstanceOf(BeanCreationException.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
this.catalog.lookup(Supplier.class, "kotlinSupplier");
|
||||
}).isInstanceOf(BeanCreationException.class);
|
||||
|
||||
}
|
||||
|
||||
private void create(Class<?>[] types, String... props) {
|
||||
this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run();
|
||||
this.catalog = this.context.getBean(FunctionCatalog.class);
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
protected static class SimpleConfiguration {
|
||||
|
||||
@Bean
|
||||
public Function<String, String> function2() {
|
||||
return value -> value + "function2";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class KotlinLambdasConfiguration {
|
||||
fun kotlinFunction(): (String) -> String {
|
||||
return { it.toUpperCase() }
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
fun kotlinPojoFunction(): (Person) -> String {
|
||||
return { it.name.toString()}
|
||||
@@ -52,4 +52,5 @@ class KotlinLambdasConfiguration {
|
||||
fun javaFunction(): Function<String, String> {
|
||||
return Function { x -> x }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import reactor.core.publisher.Flux
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
* @author Adrien Poupard
|
||||
*
|
||||
*/
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
class KotlinSuspendFlowLambdasConfiguration {
|
||||
|
||||
@Bean
|
||||
fun kotlinFunction(): suspend (Flow<String>) -> Flow<String> = { flow ->
|
||||
flow.map { value -> value.toUpperCase() }
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinPojoFunction(): suspend (Flow<Person>) -> Flow<String> = { flow ->
|
||||
flow.map(Person::toString)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinConsumer(): suspend (Flow<String>) -> Unit = { flow ->
|
||||
flow.collect(::println)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinSupplier(): suspend () -> Flow<String> = {
|
||||
flow {
|
||||
emit("Hello")
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun javaFunction(): Function<Flux<String>, Flux<String>> {
|
||||
return Function { x -> x }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/**
|
||||
* @author Adrien Poupard
|
||||
*
|
||||
*/
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
class KotlinSuspendLambdasConfiguration {
|
||||
|
||||
@Bean
|
||||
fun kotlinFunction(): suspend (Person) -> String {
|
||||
return { it.name.toString()}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinConsumer(): suspend (String) -> Unit {
|
||||
return { println(it) }
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinSupplier(): suspend () -> String {
|
||||
return { "Hello" }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate
|
||||
import org.springframework.cloud.function.web.RestApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.RequestEntity
|
||||
import org.springframework.messaging.Message
|
||||
import org.springframework.messaging.support.MessageBuilder
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* @author Adrien Poupard
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
properties = ["spring.cloud.function.web.path=/functions", "spring.main.web-application-type=reactive"]
|
||||
)
|
||||
@ContextConfiguration(classes = [RestApplication::class, HeadersToMessageSuspendTests.TestConfiguration::class])
|
||||
class HeadersToMessageSuspendTests {
|
||||
@Autowired
|
||||
private val rest: TestRestTemplate? = null
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testBodyAndCustomHeaderFromMessagePropagation() {
|
||||
// test POJO paylod
|
||||
var postForEntity = rest!!
|
||||
.exchange(
|
||||
RequestEntity.post(URI("/functions/employeeSuspend"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("[{\"name\":\"Bob\",\"age\":25}]"), String::class.java
|
||||
)
|
||||
|
||||
Assertions.assertThat(postForEntity.body).isEqualTo("[{\"name\":\"Bob\",\"age\":25}]")
|
||||
Assertions.assertThat(postForEntity.headers.containsKey("x-content-type")).isTrue
|
||||
Assertions.assertThat(postForEntity.headers["x-content-type"]!![0])
|
||||
.isEqualTo("application/xml")
|
||||
Assertions.assertThat(postForEntity.headers["foo"]!![0]).isEqualTo("bar")
|
||||
|
||||
// test simple type payload
|
||||
postForEntity = rest.postForEntity(
|
||||
URI("/functions/stringSuspend"),
|
||||
"HELLO", String::class.java
|
||||
)
|
||||
Assertions.assertThat(postForEntity.body).isEqualTo("[\"HELLO\"]")
|
||||
Assertions.assertThat(postForEntity.headers.containsKey("x-content-type")).isTrue
|
||||
Assertions.assertThat(postForEntity.headers["x-content-type"]!![0])
|
||||
.isEqualTo("application/xml")
|
||||
Assertions.assertThat(postForEntity.headers["foo"]!![0]).isEqualTo("bar")
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@org.springframework.boot.test.context.TestConfiguration
|
||||
class TestConfiguration {
|
||||
@Bean("stringSuspend")
|
||||
fun functiono():suspend (employee: Flow<Message<String>>) -> Flow<Message<String>> = { flow: Flow<Message<String>> ->
|
||||
flow.map { request ->
|
||||
val message =
|
||||
MessageBuilder.withPayload(request.payload)
|
||||
.setHeader("X-Content-Type", "application/xml")
|
||||
.setHeader("foo", "bar").build()
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
@Bean("employeeSuspend")
|
||||
fun function1(): suspend (employee: Flow<Message<Employee>>) -> Flow<Message<Employee>> = { flow ->
|
||||
flow.map { request ->
|
||||
val message =
|
||||
MessageBuilder
|
||||
.withPayload(request.payload)
|
||||
.setHeader("X-Content-Type", "application/xml")
|
||||
.setHeader("foo", "bar")
|
||||
.build()
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Employee {
|
||||
var name: String? = null
|
||||
var age = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2012-2019 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.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate
|
||||
import org.springframework.cloud.function.web.RestApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.RequestEntity
|
||||
import org.springframework.messaging.Message
|
||||
import org.springframework.messaging.support.MessageBuilder
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import java.lang.Exception
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Oleg Zhurakousky
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
properties = ["spring.cloud.function.web.path=/functions", "spring.main.web-application-type=reactive"]
|
||||
)
|
||||
@ContextConfiguration(classes = [RestApplication::class, HeadersToMessageTests.TestConfiguration::class])
|
||||
class HeadersToMessageTests {
|
||||
@Autowired
|
||||
private val rest: TestRestTemplate? = null
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testBodyAndCustomHeaderFromMessagePropagation() {
|
||||
// test POJO paylod
|
||||
var postForEntity = rest!!
|
||||
.exchange(
|
||||
RequestEntity.post(URI("/functions/employee"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("{\"name\":\"Bob\",\"age\":25}"), String::class.java
|
||||
)
|
||||
Assertions.assertThat(postForEntity.body).isEqualTo("{\"name\":\"Bob\",\"age\":25}")
|
||||
Assertions.assertThat(postForEntity.headers.containsKey("x-content-type")).isTrue
|
||||
Assertions.assertThat(postForEntity.headers["x-content-type"]!![0])
|
||||
.isEqualTo("application/xml")
|
||||
Assertions.assertThat(postForEntity.headers["foo"]!![0]).isEqualTo("bar")
|
||||
|
||||
// test simple type payload
|
||||
postForEntity = rest.postForEntity(
|
||||
URI("/functions/string"),
|
||||
"{\"name\":\"Bob\",\"age\":25}", String::class.java
|
||||
)
|
||||
Assertions.assertThat(postForEntity.body).isEqualTo("{\"name\":\"Bob\",\"age\":25}")
|
||||
Assertions.assertThat(postForEntity.headers.containsKey("x-content-type")).isTrue
|
||||
Assertions.assertThat(postForEntity.headers["x-content-type"]!![0])
|
||||
.isEqualTo("application/xml")
|
||||
Assertions.assertThat(postForEntity.headers["foo"]!![0]).isEqualTo("bar")
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@org.springframework.boot.test.context.TestConfiguration
|
||||
class TestConfiguration {
|
||||
@Bean("string")
|
||||
fun functiono(): (message: Message<String?>) -> Message<String> = { request: Message<String?> ->
|
||||
val message =
|
||||
MessageBuilder.withPayload(request.payload)
|
||||
.setHeader("X-Content-Type", "application/xml")
|
||||
.setHeader("foo", "bar").build()
|
||||
message
|
||||
}
|
||||
|
||||
@Bean("employee")
|
||||
fun function1(): (employee: Message<Employee>) -> Message<Employee> = { request ->
|
||||
val message =
|
||||
MessageBuilder
|
||||
.withPayload(request.payload)
|
||||
.setHeader("X-Content-Type", "application/xml")
|
||||
.setHeader("foo", "bar").build()
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
// used by json converter
|
||||
class Employee {
|
||||
var name: String? = null
|
||||
var age = 0
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.cloud.function.web.RestApplication;
|
||||
import org.springframework.cloud.function.web.flux.HeadersToMessageTests.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
@@ -41,12 +41,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Adrien Poupard
|
||||
*
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
|
||||
"spring.cloud.function.web.path=/functions",
|
||||
"spring.main.web-application-type=reactive" })
|
||||
@ContextConfiguration(classes = { RestApplication.class, TestConfiguration.class })
|
||||
@ContextConfiguration(classes = { RestApplication.class, HeadersToMessageTests.TestConfiguration.class })
|
||||
public class HeadersToMessageTests {
|
||||
|
||||
@Autowired
|
||||
|
||||
Reference in New Issue
Block a user