diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java index ff3988182..73840fb83 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java @@ -37,6 +37,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; @@ -119,23 +120,28 @@ public class FunctionInvoker { Object enhancedInput = enhanceInputIfNecessary(input, executionContext); Object output = function.apply(enhancedInput); - if (output instanceof Publisher && !function.isOutputTypePublisher()) { - List resultList = new ArrayList<>(); - for (Object resultItem : Flux.from((Publisher) output).toIterable()) { - if (resultItem instanceof Collection) { - resultList.addAll((Collection) resultItem); - } - else { - if (Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getInputType())) - && !Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getOutputType()))) { - return (O) this.convertOutputIfNecessary(input, resultItem); + if (output instanceof Publisher) { + if (FunctionTypeUtils.isMono(function.getOutputType())) { + return (O) this.convertOutputIfNecessary(input, Mono.from((Publisher) output).blockOptional().get()); + } + else { + List resultList = new ArrayList<>(); + for (Object resultItem : Flux.from((Publisher) output).toIterable()) { + if (resultItem instanceof Collection) { + resultList.addAll((Collection) resultItem); } else { - resultList.add(resultItem); + if (Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getInputType())) + && !Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getOutputType()))) { + return (O) this.convertOutputIfNecessary(input, resultItem); + } + else { + resultList.add(resultItem); + } } } + return (O) this.convertOutputIfNecessary(input, resultList); } - return (O) this.convertOutputIfNecessary(input, resultList); } return (O) this.convertOutputIfNecessary(input, output); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java index d4d26eec8..1f31d49ce 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java @@ -28,6 +28,7 @@ import com.microsoft.azure.functions.ExecutionContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -144,6 +145,17 @@ public class FunctionInvokerTests { assertThat(consumerResult).isEqualTo("foo1"); } + @Test + public void testReactiveFunctions() { + FunctionInvoker handler = handler(ReactiveFunctionConfiguration.class); + String result = handler.handleRequest("hello", new TestExecutionContext("uppercaseMono")); + + System.out.println(result); + +// assertThat(result).isNull(); +// assertThat(consumerResult).isEqualTo("foo1"); + } + @AfterEach public void close() throws IOException { if (this.handler != null) { @@ -161,6 +173,22 @@ public class FunctionInvokerTests { } + @Configuration + @EnableAutoConfiguration + protected static class ReactiveFunctionConfiguration { + + @Bean + public Function, Flux> echoStream() { + return f -> f; + } + + @Bean + public Function, Mono> uppercaseMono() { + return f -> f.map(v -> v.toUpperCase()); + } + + } + @Configuration protected static class NonFluxSupplierConfig { diff --git a/spring-cloud-function-samples/function-sample-azure/README.adoc b/spring-cloud-function-samples/function-sample-azure/README.adoc index c8d9d6313..66cc7dbad 100644 --- a/spring-cloud-function-samples/function-sample-azure/README.adoc +++ b/spring-cloud-function-samples/function-sample-azure/README.adoc @@ -37,6 +37,9 @@ $ http POST localhost:7071/api/uppercase greeting=hello name='your name' The same is for `echo` function, however it will take any input since all it does is just echos it back. +There is also a reactive version of 'uppercase' - `uppercaseReactive` which will produce the same result, but +demonstrates and validates the ability to use reactive functions with Azure. + To run locally on top of Azure Functions, and to deploy to your live Azure environment, you will need the Azure Functions Core Tools installed along with the Azure CLI (see https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-java-maven for more details). To deploy the function to your live Azure environment, including an automatic provisioning of an HTTPTrigger for the function: diff --git a/spring-cloud-function-samples/function-sample-azure/pom.xml b/spring-cloud-function-samples/function-sample-azure/pom.xml index 3fa46faaf..fa48d9d2b 100644 --- a/spring-cloud-function-samples/function-sample-azure/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure/pom.xml @@ -54,7 +54,7 @@ org.springframework.cloud spring-cloud-function-dependencies - 3.1.2-SNAPSHOT + 3.1.3-SNAPSHOT pom import diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java index 72e77958d..beafa72af 100644 --- a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java @@ -27,6 +27,8 @@ import org.springframework.messaging.Message; import com.microsoft.azure.functions.ExecutionContext; +import reactor.core.publisher.Mono; + @SpringBootApplication public class Config { @@ -65,5 +67,13 @@ public class Config { }; } + + @Bean + public Function, Mono> uppercaseReactive() { + return mono -> mono.map(value -> { + return value.toUpperCase(); + }); + } + } diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveUppercaseHandler.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveUppercaseHandler.java new file mode 100644 index 000000000..cb82087b5 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveUppercaseHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 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 example; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; + +import java.util.Optional; + +import org.springframework.cloud.function.adapter.azure.FunctionInvoker; + +/** + * @author Oleg Zhurakousky + */ +public class ReactiveUppercaseHandler extends FunctionInvoker { + + @FunctionName("uppercaseReactive") + public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, + HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + ExecutionContext context) { + return handleRequest(request.getBody().get(), context); + } + +}