From 87f077954ef97467d28c1cf03dc2c00ba9604b5d Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 24 May 2017 11:25:54 +0100 Subject: [PATCH] Try to detect single-valued returns in AWS Lambdas generally only deal with single values anyway, but for consistency we should return similar results from a Lambda handler as from an MVC handler in s-c-f-web. --- .../FunctionInvokingKinesisEventHandler.java | 52 ----------- ...ava => SpringBootKinesisEventHandler.java} | 19 +++- .../adapter/aws/SpringBootRequestHandler.java | 18 ++-- .../adapter/aws/SpringBootStreamHandler.java | 11 ++- .../aws/SpringBootRequestHandlerTests.java | 91 +++++++++++++++++++ .../aws/SpringBootStreamHandlerTests.java | 91 +++++++++++++++++++ 6 files changed, 215 insertions(+), 67 deletions(-) delete mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvokingKinesisEventHandler.java rename spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/{FunctionInvokingS3EventHandler.java => SpringBootKinesisEventHandler.java} (57%) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandlerTests.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandlerTests.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvokingKinesisEventHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvokingKinesisEventHandler.java deleted file mode 100644 index 444408db5..000000000 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvokingKinesisEventHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.adapter.aws; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Date; - -import com.amazonaws.services.lambda.runtime.events.KinesisEvent; -import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; - -/** - * @author Mark Fisher - */ -public class FunctionInvokingKinesisEventHandler extends SpringBootRequestHandler { - - public FunctionInvokingKinesisEventHandler(Class configurationClass) { - super(configurationClass); - } - - @Override - protected String convertEvent(KinesisEvent event) { - StringBuilder result = new StringBuilder(); - for (KinesisEventRecord record : event.getRecords()) { - String id = record.getEventID(); - String name = record.getEventName(); - String source = record.getEventSource(); - Date timestamp = record.getKinesis().getApproximateArrivalTimestamp(); - String partitionKey = record.getKinesis().getPartitionKey(); - String sequenceNumber = record.getKinesis().getSequenceNumber(); - ByteBuffer data = record.getKinesis().getData(); - String dataString = new String(data.array(), Charset.forName("UTF-8")); - result.append(String.format("id=%s,name=%s,source=%s,timestamp=%s,partitionKey=%s,sequenceNumber=%s,data=%s", - id, name, source, timestamp, partitionKey, sequenceNumber, dataString)); - } - return result.toString(); - } -} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvokingS3EventHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootKinesisEventHandler.java similarity index 57% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvokingS3EventHandler.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootKinesisEventHandler.java index 0b3db676a..8e3638a24 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvokingS3EventHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootKinesisEventHandler.java @@ -16,19 +16,28 @@ package org.springframework.cloud.function.adapter.aws; -import com.amazonaws.services.lambda.runtime.events.S3Event; +import java.util.List; + +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; /** * @author Mark Fisher */ -public class FunctionInvokingS3EventHandler extends SpringBootRequestHandler { +public class SpringBootKinesisEventHandler + extends SpringBootRequestHandler { - public FunctionInvokingS3EventHandler(Class configurationClass) { + public SpringBootKinesisEventHandler() { + super(); + } + + public SpringBootKinesisEventHandler(Class configurationClass) { super(configurationClass); } @Override - protected String convertEvent(S3Event event) { - return event.toJson(); + protected List convertEvent(KinesisEvent event) { + // TODO: maybe convert to List + return event.getRecords(); } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java index c405f5181..ec1bdec8f 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java @@ -28,7 +28,7 @@ import reactor.core.publisher.Flux; /** * @author Mark Fisher */ -public class SpringBootRequestHandler extends SpringFunctionInitializer implements RequestHandler> { +public class SpringBootRequestHandler extends SpringFunctionInitializer implements RequestHandler { public SpringBootRequestHandler(Class configurationClass) { super(configurationClass); @@ -39,24 +39,26 @@ public class SpringBootRequestHandler extends SpringFunctionInitializer im } @Override - public List handleRequest(E event, Context context) { + public Object handleRequest(E event, Context context) { initialize(); Object input = convertEvent(event); Flux output = apply(extract(input)); - return result(output); + return result(input, output); } - private List result(Flux output) { + private Object result(Object input, Flux output) { List result = new ArrayList<>(); for (Object value : output.toIterable()) { result.add(value); } - return convertResult(result); + if (isSingleValue(input) && result.size()==1) { + return result.get(0); + } + return result; } - @SuppressWarnings("unchecked") - protected List convertResult(List value) { - return (List) value; + private boolean isSingleValue(Object input) { + return !(input instanceof Collection); } private Flux extract(Object input) { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java index 3598f2299..b3510b486 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java @@ -54,17 +54,24 @@ public class SpringBootStreamHandler extends SpringFunctionInitializer initialize(); Object value = convertStream(input); Flux flux = apply(extract(value)); - mapper.writeValue(output, result(flux)); + mapper.writeValue(output, result(value, flux)); } - private Object result(Flux flux) { + private Object result(Object input, Flux flux) { List result = new ArrayList<>(); for (Object value : flux.toIterable()) { result.add(value); } + if (isSingleValue(input) && result.size()==1) { + return result.get(0); + } return result; } + private boolean isSingleValue(Object input) { + return !(input instanceof Collection); + } + private Flux extract(Object input) { if (input instanceof Collection) { return Flux.fromIterable((Iterable) input); diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandlerTests.java new file mode 100644 index 000000000..aa7e7fceb --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandlerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.adapter.aws; + +import java.util.function.Function; + +import org.junit.Test; + +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +public class SpringBootRequestHandlerTests { + + private SpringBootRequestHandler handler; + + @Test + public void functionBean() throws Exception { + handler = new SpringBootRequestHandler(FunctionConfig.class); + handler.initialize(); + Object output = handler.handleRequest(new Foo("foo"), null); + assertThat(output).isInstanceOf(Bar.class); + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class, + JacksonAutoConfiguration.class }) + protected static class FunctionConfig { + @Bean + public Function function() { + return foo -> new Bar(foo.getValue().toUpperCase()); + } + } + + protected static class Foo { + private String value; + + public Foo(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + protected static class Bar { + private String value; + + public Bar() { + } + + public Bar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandlerTests.java new file mode 100644 index 000000000..d9401f4b6 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandlerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.adapter.aws; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.function.Function; + +import org.junit.Test; + +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +public class SpringBootStreamHandlerTests { + + private SpringBootStreamHandler handler; + + @Test + public void functionBean() throws Exception { + handler = new SpringBootStreamHandler(FunctionConfig.class); + handler.initialize(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(new ByteArrayInputStream("{\"value\":\"foo\"}".getBytes()), + output, null); + assertThat(output.toString()).isEqualTo("{\"value\":\"FOO\"}"); + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class, + JacksonAutoConfiguration.class }) + protected static class FunctionConfig { + @Bean + public Function function() { + return foo -> new Bar(foo.getValue().toUpperCase()); + } + } + + protected static class Foo { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + protected static class Bar { + private String value; + + public Bar() { + } + + public Bar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +}