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.
This commit is contained in:
Dave Syer
2017-05-24 11:25:54 +01:00
parent 5589804d2c
commit 87f077954e
6 changed files with 215 additions and 67 deletions

View File

@@ -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<KinesisEvent, String> {
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();
}
}

View File

@@ -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<S3Event, String> {
public class SpringBootKinesisEventHandler
extends SpringBootRequestHandler<KinesisEvent, String> {
public FunctionInvokingS3EventHandler(Class<?> configurationClass) {
public SpringBootKinesisEventHandler() {
super();
}
public SpringBootKinesisEventHandler(Class<?> configurationClass) {
super(configurationClass);
}
@Override
protected String convertEvent(S3Event event) {
return event.toJson();
protected List<KinesisEventRecord> convertEvent(KinesisEvent event) {
// TODO: maybe convert to List<Message>
return event.getRecords();
}
}

View File

@@ -28,7 +28,7 @@ import reactor.core.publisher.Flux;
/**
* @author Mark Fisher
*/
public class SpringBootRequestHandler<E, O> extends SpringFunctionInitializer implements RequestHandler<E, List<O>> {
public class SpringBootRequestHandler<E, O> extends SpringFunctionInitializer implements RequestHandler<E, Object> {
public SpringBootRequestHandler(Class<?> configurationClass) {
super(configurationClass);
@@ -39,24 +39,26 @@ public class SpringBootRequestHandler<E, O> extends SpringFunctionInitializer im
}
@Override
public List<O> 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<O> result(Flux<?> output) {
private Object result(Object input, Flux<?> output) {
List<Object> 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<O> convertResult(List<Object> value) {
return (List<O>) value;
private boolean isSingleValue(Object input) {
return !(input instanceof Collection);
}
private Flux<?> extract(Object input) {

View File

@@ -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<Object> 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);

View File

@@ -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<Foo, Bar> handler;
@Test
public void functionBean() throws Exception {
handler = new SpringBootRequestHandler<Foo, Bar>(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<Foo, Bar> 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;
}
}
}

View File

@@ -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<Foo, Bar> 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;
}
}
}