From e35546ffd74fcd0b3d026aac4f3eda87b94727d6 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 17 Feb 2022 17:30:53 +0100 Subject: [PATCH 01/15] use serialization lib instead of internal stuff --- pom.xml | 6 ++++++ .../lambda/powertools/utilities/JsonConfig.java | 8 +------- powertools-validation/pom.xml | 5 +++++ .../validation/internal/ValidationAspectTest.java | 14 +++++++++----- .../src/test/resources/kinesis.json | 2 +- powertools-validation/src/test/resources/sqs.json | 2 +- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index 3e1892e95..ac2d84a4f 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ UTF-8 1.2.1 3.11.0 + 1.0.0 3.10.0 1.14.0 2.22.2 @@ -122,6 +123,11 @@ aws-lambda-java-events ${lambda.events.version} + + com.amazonaws + aws-lambda-java-serialization + ${lambda.serial.version} + software.amazon.awssdk bom diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java index c3a5fc865..d8af1c0cb 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java @@ -24,8 +24,6 @@ import software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction; import software.amazon.lambda.powertools.utilities.jmespath.JsonFunction; -import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; - public class JsonConfig { private JsonConfig() { } @@ -38,11 +36,7 @@ public static JsonConfig get() { return ConfigHolder.instance; } - private static final ThreadLocal om = ThreadLocal.withInitial(() -> { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); - return objectMapper; - }); + private static final ThreadLocal om = ThreadLocal.withInitial(ObjectMapper::new); private final FunctionRegistry defaultFunctions = FunctionRegistry.defaultRegistry(); private final FunctionRegistry customFunctions = defaultFunctions.extend( diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index db73fb12d..721f30a28 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -87,6 +87,11 @@ junit-jupiter-engine test + + com.amazonaws + aws-lambda-java-serialization + test + org.apache.commons commons-lang3 diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 2bbe7cdaa..c8741f1e3 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -18,12 +18,14 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; import com.amazonaws.services.lambda.runtime.events.KinesisEvent; import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import software.amazon.lambda.powertools.validation.ValidationException; import software.amazon.lambda.powertools.validation.ValidationConfig; +import software.amazon.lambda.powertools.validation.ValidationException; import software.amazon.lambda.powertools.validation.handlers.*; import software.amazon.lambda.powertools.validation.model.MyCustomEvent; @@ -91,16 +93,18 @@ public void validate_inputKO_schemaInString_shouldThrowValidationException() { } @Test - public void validate_SQS() throws IOException { - SQSEvent event = ValidationConfig.get().getObjectMapper().readValue(this.getClass().getResourceAsStream("/sqs.json"), SQSEvent.class); + public void validate_SQS() { + PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); + SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs.json")); SQSHandler handler = new SQSHandler(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } @Test - public void validate_Kinesis() throws IOException { - KinesisEvent event = ValidationConfig.get().getObjectMapper().readValue(this.getClass().getResourceAsStream("/kinesis.json"), KinesisEvent.class); + public void validate_Kinesis() { + PojoSerializer pojoSerializer = LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); + KinesisEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/kinesis.json")); KinesisHandler handler = new KinesisHandler(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); diff --git a/powertools-validation/src/test/resources/kinesis.json b/powertools-validation/src/test/resources/kinesis.json index 6d99be7e5..ad33ae456 100644 --- a/powertools-validation/src/test/resources/kinesis.json +++ b/powertools-validation/src/test/resources/kinesis.json @@ -1,5 +1,5 @@ { - "records": [ + "Records": [ { "kinesis": { "partitionKey": "partitionKey-03", diff --git a/powertools-validation/src/test/resources/sqs.json b/powertools-validation/src/test/resources/sqs.json index 9180c5839..129e79bb2 100644 --- a/powertools-validation/src/test/resources/sqs.json +++ b/powertools-validation/src/test/resources/sqs.json @@ -1,5 +1,5 @@ { - "records": [ + "Records": [ { "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db2", "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", From 7008023baa337031ffab30ddf3815b3e92018390 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 17 Feb 2022 17:31:44 +0100 Subject: [PATCH 02/15] add ActiveMq & RabbitMQ --- .../validation/internal/ValidationAspect.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index b42ce71ab..0a1f00599 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -91,8 +91,14 @@ && placedOnRequestHandler(pjp)) { event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); } else if (obj instanceof KafkaEvent) { KafkaEvent event = (KafkaEvent) obj; - event.getRecords().forEach((s, records) -> records.forEach(record -> validate(record.getValue(), inboundJsonSchema))); - }else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { + event.getRecords().forEach((s, records) -> records.forEach(record -> validate(decode(record.getValue()), inboundJsonSchema))); + } else if (obj instanceof ActiveMQEvent) { + ActiveMQEvent event = (ActiveMQEvent) obj; + event.getMessages().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + } else if (obj instanceof RabbitMQEvent) { + RabbitMQEvent event = (RabbitMQEvent) obj; + event.getRmqMessagesByQueue().forEach((s, records) -> records.forEach(record -> validate(decode(record.getData()), inboundJsonSchema))); + } else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj; event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); } else if (obj instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { From c5e04d9fa1e25bf8a25815d612ae549b19718224 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 17 Feb 2022 17:32:43 +0100 Subject: [PATCH 03/15] feature: easy deserialization of event content --- powertools-serialization/pom.xml | 13 ++ .../EventDeserializationException.java | 26 +++ .../utilities/EventDeserializer.java | 150 ++++++++++++++++++ .../utilities/EventDeserializerTest.java | 125 +++++++++++++++ .../powertools/utilities/model/Basket.java | 55 +++++++ .../powertools/utilities/model/Product.java | 70 ++++++++ .../src/test/resources/apigw_event.json | 62 ++++++++ .../src/test/resources/kafka_event.json | 27 ++++ .../src/test/resources/kinesis_event.json | 38 +++++ .../src/test/resources/sns_event.json | 27 ++++ .../src/test/resources/sqs_event.json | 40 +++++ spotbugs-exclude.xml | 8 + 12 files changed, 641 insertions(+) create mode 100644 powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.java create mode 100644 powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java create mode 100644 powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java create mode 100644 powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Basket.java create mode 100644 powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Product.java create mode 100644 powertools-serialization/src/test/resources/apigw_event.json create mode 100644 powertools-serialization/src/test/resources/kafka_event.json create mode 100644 powertools-serialization/src/test/resources/kinesis_event.json create mode 100644 powertools-serialization/src/test/resources/sns_event.json create mode 100644 powertools-serialization/src/test/resources/sqs_event.json diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index 5cff32313..e460b7022 100644 --- a/powertools-serialization/pom.xml +++ b/powertools-serialization/pom.xml @@ -45,6 +45,14 @@ io.burt jmespath-jackson + + com.amazonaws + aws-lambda-java-events + + + org.apache.logging.log4j + log4j-slf4j-impl + @@ -57,6 +65,11 @@ assertj-core test + + com.amazonaws + aws-lambda-java-tests + test + diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.java new file mode 100644 index 000000000..2b06c9256 --- /dev/null +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializationException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. + * 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 software.amazon.lambda.powertools.utilities; + +public class EventDeserializationException extends RuntimeException { + private static final long serialVersionUID = -5003158148870110442L; + + public EventDeserializationException(String msg, Exception e) { + super(msg, e); + } + + public EventDeserializationException(String msg) { + super(msg); + } +} diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java new file mode 100644 index 000000000..76b5e98e2 --- /dev/null +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java @@ -0,0 +1,150 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. + * 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 software.amazon.lambda.powertools.utilities; + +import com.amazonaws.services.lambda.runtime.events.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; +import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress; + +public class EventDeserializer { + + private static final Logger LOG = LoggerFactory.getLogger(EventDeserializer.class); + + public static EventPart from(Object obj) { + if (obj instanceof String) { + return new EventPart((String) obj); + } else if (obj instanceof Map) { + return new EventPart((Map) obj); + } else if (obj instanceof APIGatewayProxyRequestEvent) { + APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; + return new EventPart(event.getBody()); + } else if (obj instanceof APIGatewayV2HTTPEvent) { + APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; + return new EventPart(event.getBody()); + } else if (obj instanceof SNSEvent) { + SNSEvent event = (SNSEvent) obj; + return new EventPart(event.getRecords().get(0).getSNS().getMessage()); + } else if (obj instanceof SQSEvent) { + SQSEvent event = (SQSEvent) obj; + return new EventPart(event.getRecords().stream().map(SQSEvent.SQSMessage::getBody).collect(Collectors.toList())); + } else if (obj instanceof ScheduledEvent) { + ScheduledEvent event = (ScheduledEvent) obj; + return new EventPart(event.getDetail()); + } else if (obj instanceof ApplicationLoadBalancerRequestEvent) { + ApplicationLoadBalancerRequestEvent event = (ApplicationLoadBalancerRequestEvent) obj; + return new EventPart(event.getBody()); + } else if (obj instanceof CloudWatchLogsEvent) { + CloudWatchLogsEvent event = (CloudWatchLogsEvent) obj; + return new EventPart(decompress(decode(event.getAwsLogs().getData().getBytes(UTF_8)))); + } else if (obj instanceof CloudFormationCustomResourceEvent) { + CloudFormationCustomResourceEvent event = (CloudFormationCustomResourceEvent) obj; + return new EventPart(event.getResourceProperties()); + } else if (obj instanceof KinesisEvent) { + KinesisEvent event = (KinesisEvent) obj; + return new EventPart(event.getRecords().stream().map(r -> decode(r.getKinesis().getData())).collect(Collectors.toList())); + } else if (obj instanceof KinesisFirehoseEvent) { + KinesisFirehoseEvent event = (KinesisFirehoseEvent) obj; + return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList())); + } else if (obj instanceof KafkaEvent) { + KafkaEvent event = (KafkaEvent) obj; + return new EventPart(event.getRecords().values().stream().flatMap(List::stream).map(r -> decode(r.getValue())).collect(Collectors.toList())); + } else if (obj instanceof ActiveMQEvent) { + ActiveMQEvent event = (ActiveMQEvent) obj; + return new EventPart(event.getMessages().stream().map(m -> decode(m.getData())).collect(Collectors.toList())); + } else if (obj instanceof RabbitMQEvent) { + RabbitMQEvent event = (RabbitMQEvent) obj; + return new EventPart(event.getRmqMessagesByQueue().values().stream().flatMap(List::stream).map(r -> decode(r.getData())).collect(Collectors.toList())); + } else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { + KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj; + return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList())); + } else if (obj instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { + KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) obj; + return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList())); + } else { + // does not really make sense to use this EventLoader when you already have a typed object + // just not to throw an exception + LOG.warn("Consider using your object directly instead of using EventDeserializer"); + return new EventPart(obj); + } + } + + public static class EventPart { + private Map contentMap; + private String content; + private List contentList; + private Object contentObject; + + public EventPart(List contentList) { + this.contentList = contentList; + } + + public EventPart(String content) { + this.content = content; + } + + public EventPart(Map contentMap) { + this.contentMap = contentMap; + } + + public EventPart(Object content) { + this.contentObject = content; + } + + public T extractDataAs(Class clazz) { + try { + if (content != null) { + if (content.getClass().equals(clazz)) { + // do not read json when returning String, just return the String + return (T) content; + } + return JsonConfig.get().getObjectMapper().reader().readValue(content, clazz); + } + if (contentMap != null) { + return JsonConfig.get().getObjectMapper().convertValue(contentMap, clazz); + } + if (contentObject != null) { + return (T) contentObject; + } + if (contentList != null) { + throw new EventDeserializationException("The content of this event is a list, consider using 'extractDataAsListOf' instead"); + } + throw new EventDeserializationException("Event content is null"); + } catch (IOException e) { + throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e); + } + } + + public List extractDataAsListOf(Class clazz) { + if (contentList == null) { + throw new EventDeserializationException("Event content is null"); + } + return contentList.stream().map(s -> { + try { + return JsonConfig.get().getObjectMapper().reader().readValue(s, clazz); + } catch (IOException e) { + throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e); + } + }).collect(Collectors.toList()); + } + } +} diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java new file mode 100644 index 000000000..8c427e69f --- /dev/null +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. + * 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 software.amazon.lambda.powertools.utilities; + +import com.amazonaws.services.lambda.runtime.events.*; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import software.amazon.lambda.powertools.utilities.model.Basket; +import software.amazon.lambda.powertools.utilities.model.Product; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.lambda.powertools.utilities.EventDeserializer.from; + +public class EventDeserializerTest { + + @Test + public void testDeserializeStringAsString_shouldReturnString() { + String stringEvent = "Hello World"; + String result = from(stringEvent).extractDataAs(String.class); + assertThat(result).isEqualTo(stringEvent); + } + + @Test + public void testDeserializeStringAsObject_shouldReturnObject() { + String productStr = "{\"id\":1234, \"name\":\"product\", \"price\":42}"; + Product product = from(productStr).extractDataAs(Product.class); + assertProduct(product); + } + + @Test + public void testDeserializeMapAsObject_shouldReturnObject() { + Map map = new HashMap<>(); + map.put("id", 1234); + map.put("name", "product"); + map.put("price", 42); + Product product = from(map).extractDataAs(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) + public void testDeserializeAPIGWEventBodyAsObject_shouldReturnObject(APIGatewayProxyRequestEvent event) { + Product product = from(event).extractDataAs(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) + public void testDeserializeAPIGWEventBodyAsWrongObjectType_shouldThrowException(APIGatewayProxyRequestEvent event) { + assertThatThrownBy(() -> from(event).extractDataAs(Basket.class)) + .isInstanceOf(EventDeserializationException.class) + .hasMessage("Cannot load the event as Basket"); + } + + @ParameterizedTest + @Event(value = "sns_event.json", type = SNSEvent.class) + public void testDeserializeSNSEventMessageAsObject_shouldReturnObject(SNSEvent event) { + Product product = from(event).extractDataAs(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "sqs_event.json", type = SQSEvent.class) + public void testDeserializeSQSEventMessageAsList_shouldReturnList(SQSEvent event) { + List products = from(event).extractDataAsListOf(Product.class); + assertThat(products).hasSize(2); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "kinesis_event.json", type = KinesisEvent.class) + public void testDeserializeKinesisEventMessageAsList_shouldReturnList(KinesisEvent event) { + List products = from(event).extractDataAsListOf(Product.class); + assertThat(products).hasSize(2); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "kafka_event.json", type = KafkaEvent.class) + public void testDeserializeKafkaEventMessageAsList_shouldReturnList(KafkaEvent event) { + List products = from(event).extractDataAsListOf(Product.class); + assertThat(products).hasSize(2); + assertProduct(products.get(0)); + } + + @ParameterizedTest + @Event(value = "sqs_event.json", type = SQSEvent.class) + public void testDeserializeSQSEventMessageAsObject_shouldThrowException(SQSEvent event) { + assertThatThrownBy(() -> from(event).extractDataAs(Product.class)) + .isInstanceOf(EventDeserializationException.class) + .hasMessageContaining("consider using 'extractDataAsListOf' instead"); + } + + @Test + public void testDeserializeProductAsProduct_shouldReturnProduct() { + Product myProduct = new Product(1234, "product", 42); + Product product = from(myProduct).extractDataAs(Product.class); + assertProduct(product); + } + + + private void assertProduct(Product product) { + assertThat(product.getId()).isEqualTo(1234); + assertThat(product.getName()).isEqualTo("product"); + assertThat(product.getPrice()).isEqualTo(42); + } + +} diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Basket.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Basket.java new file mode 100644 index 000000000..228089c52 --- /dev/null +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Basket.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. + * 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 software.amazon.lambda.powertools.utilities.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Basket { + private List products = new ArrayList<>(); + + public List getProducts() { + return products; + } + + public void setProducts(List products) { + this.products = products; + } + + public Basket() { + } + + public Basket( Product ...p){ + products.addAll(Arrays.asList(p)); + } + + public void add(Product product) { + products.add(product); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Basket basket = (Basket) o; + return products.equals(basket.products); + } + + @Override + public int hashCode() { + return Objects.hash(products); + } +} diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Product.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Product.java new file mode 100644 index 000000000..f03f6d426 --- /dev/null +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/model/Product.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. + * 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 software.amazon.lambda.powertools.utilities.model; + +import java.util.Objects; + +public class Product { + private long id; + + private String name; + + private double price; + + public Product() { + } + + public Product(long id, String name, double price) { + this.id = id; + this.name = name; + this.price = price; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Product product = (Product) o; + return id == product.id && Double.compare(product.price, price) == 0 && Objects.equals(name, product.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, price); + } +} diff --git a/powertools-serialization/src/test/resources/apigw_event.json b/powertools-serialization/src/test/resources/apigw_event.json new file mode 100644 index 000000000..7758cb0bb --- /dev/null +++ b/powertools-serialization/src/test/resources/apigw_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"id\":1234, \"name\":\"product\", \"price\":42}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/powertools-serialization/src/test/resources/kafka_event.json b/powertools-serialization/src/test/resources/kafka_event.json new file mode 100644 index 000000000..cf1bad615 --- /dev/null +++ b/powertools-serialization/src/test/resources/kafka_event.json @@ -0,0 +1,27 @@ +{ + "eventSource": "aws:kafka", + "eventSourceArn": "arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929", + "bootstrapServers": "b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records": { + "mytopic-01": [ + { + "topic": "mytopic1", + "partition": 0, + "offset": 15, + "timestamp": 1596480920837, + "timestampType": "CREATE_TIME", + "value": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=" + } + ], + "mytopic-02": [ + { + "topic": "mytopic2", + "partition": 0, + "offset": 15, + "timestamp": 1596480920838, + "timestampType": "CREATE_TIME", + "value": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==" + } + ] + } +} diff --git a/powertools-serialization/src/test/resources/kinesis_event.json b/powertools-serialization/src/test/resources/kinesis_event.json new file mode 100644 index 000000000..5b95ddaf4 --- /dev/null +++ b/powertools-serialization/src/test/resources/kinesis_event.json @@ -0,0 +1,38 @@ +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/sns_event.json b/powertools-serialization/src/test/resources/sns_event.json new file mode 100644 index 000000000..317a657d9 --- /dev/null +++ b/powertools-serialization/src/test/resources/sns_event.json @@ -0,0 +1,27 @@ +{ + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:eu-central-1:123456789012:TopicSendToMe:e3ddc7d5-2f86-40b8-a13d-3362f94fd8dd", + "Sns": { + "Type": "Notification", + "MessageId": "dc918f50-80c6-56a2-ba33-d8a9bbf013ab", + "TopicArn": "arn:aws:sns:eu-central-1:123456789012:TopicSendToMe", + "Subject": "Test sns message", + "Message": "{\"id\":1234, \"name\":\"product\", \"price\":42}", + "Timestamp": "2020-10-08T16:06:14.656Z", + "SignatureVersion": "1", + "Signature": "UWnPpkqPAphyr+6PXzUF9++4zJcw==", + "SigningCertUrl": "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem", + "UnsubscribeUrl": "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe", + "MessageAttributes": { + "name": { + "Type": "String", + "Value": "Bob" + } + } + } + } + ] +} \ No newline at end of file diff --git a/powertools-serialization/src/test/resources/sqs_event.json b/powertools-serialization/src/test/resources/sqs_event.json new file mode 100644 index 000000000..d33db4b53 --- /dev/null +++ b/powertools-serialization/src/test/resources/sqs_event.json @@ -0,0 +1,40 @@ +{ + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1234,\n \"name\": \"product\",\n \"price\": 42\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 12345,\n \"name\": \"product5\",\n \"price\": 45\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 30e627a56..0c8d1d8f8 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -77,6 +77,14 @@ + + + + + + + + From 27d1fc0a50cf5bc99d09f90d6d3296d8401210f9 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Thu, 17 Feb 2022 18:16:27 +0100 Subject: [PATCH 04/15] rename methods in deserializer --- .../utilities/EventDeserializer.java | 6 ++--- .../utilities/EventDeserializerTest.java | 24 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java index 76b5e98e2..3bf92b55b 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java @@ -30,7 +30,7 @@ public class EventDeserializer { private static final Logger LOG = LoggerFactory.getLogger(EventDeserializer.class); - public static EventPart from(Object obj) { + public static EventPart extractDataFrom(Object obj) { if (obj instanceof String) { return new EventPart((String) obj); } else if (obj instanceof Map) { @@ -110,7 +110,7 @@ public EventPart(Object content) { this.contentObject = content; } - public T extractDataAs(Class clazz) { + public T as(Class clazz) { try { if (content != null) { if (content.getClass().equals(clazz)) { @@ -134,7 +134,7 @@ public T extractDataAs(Class clazz) { } } - public List extractDataAsListOf(Class clazz) { + public List asListOf(Class clazz) { if (contentList == null) { throw new EventDeserializationException("Event content is null"); } diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index 8c427e69f..da4650baf 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -26,21 +26,21 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static software.amazon.lambda.powertools.utilities.EventDeserializer.from; +import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; public class EventDeserializerTest { @Test public void testDeserializeStringAsString_shouldReturnString() { String stringEvent = "Hello World"; - String result = from(stringEvent).extractDataAs(String.class); + String result = extractDataFrom(stringEvent).as(String.class); assertThat(result).isEqualTo(stringEvent); } @Test public void testDeserializeStringAsObject_shouldReturnObject() { String productStr = "{\"id\":1234, \"name\":\"product\", \"price\":42}"; - Product product = from(productStr).extractDataAs(Product.class); + Product product = extractDataFrom(productStr).as(Product.class); assertProduct(product); } @@ -50,21 +50,21 @@ public void testDeserializeMapAsObject_shouldReturnObject() { map.put("id", 1234); map.put("name", "product"); map.put("price", 42); - Product product = from(map).extractDataAs(Product.class); + Product product = extractDataFrom(map).as(Product.class); assertProduct(product); } @ParameterizedTest @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) public void testDeserializeAPIGWEventBodyAsObject_shouldReturnObject(APIGatewayProxyRequestEvent event) { - Product product = from(event).extractDataAs(Product.class); + Product product = extractDataFrom(event).as(Product.class); assertProduct(product); } @ParameterizedTest @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) public void testDeserializeAPIGWEventBodyAsWrongObjectType_shouldThrowException(APIGatewayProxyRequestEvent event) { - assertThatThrownBy(() -> from(event).extractDataAs(Basket.class)) + assertThatThrownBy(() -> extractDataFrom(event).as(Basket.class)) .isInstanceOf(EventDeserializationException.class) .hasMessage("Cannot load the event as Basket"); } @@ -72,14 +72,14 @@ public void testDeserializeAPIGWEventBodyAsWrongObjectType_shouldThrowException( @ParameterizedTest @Event(value = "sns_event.json", type = SNSEvent.class) public void testDeserializeSNSEventMessageAsObject_shouldReturnObject(SNSEvent event) { - Product product = from(event).extractDataAs(Product.class); + Product product = extractDataFrom(event).as(Product.class); assertProduct(product); } @ParameterizedTest @Event(value = "sqs_event.json", type = SQSEvent.class) public void testDeserializeSQSEventMessageAsList_shouldReturnList(SQSEvent event) { - List products = from(event).extractDataAsListOf(Product.class); + List products = extractDataFrom(event).asListOf(Product.class); assertThat(products).hasSize(2); assertProduct(products.get(0)); } @@ -87,7 +87,7 @@ public void testDeserializeSQSEventMessageAsList_shouldReturnList(SQSEvent event @ParameterizedTest @Event(value = "kinesis_event.json", type = KinesisEvent.class) public void testDeserializeKinesisEventMessageAsList_shouldReturnList(KinesisEvent event) { - List products = from(event).extractDataAsListOf(Product.class); + List products = extractDataFrom(event).asListOf(Product.class); assertThat(products).hasSize(2); assertProduct(products.get(0)); } @@ -95,7 +95,7 @@ public void testDeserializeKinesisEventMessageAsList_shouldReturnList(KinesisEve @ParameterizedTest @Event(value = "kafka_event.json", type = KafkaEvent.class) public void testDeserializeKafkaEventMessageAsList_shouldReturnList(KafkaEvent event) { - List products = from(event).extractDataAsListOf(Product.class); + List products = extractDataFrom(event).asListOf(Product.class); assertThat(products).hasSize(2); assertProduct(products.get(0)); } @@ -103,7 +103,7 @@ public void testDeserializeKafkaEventMessageAsList_shouldReturnList(KafkaEvent e @ParameterizedTest @Event(value = "sqs_event.json", type = SQSEvent.class) public void testDeserializeSQSEventMessageAsObject_shouldThrowException(SQSEvent event) { - assertThatThrownBy(() -> from(event).extractDataAs(Product.class)) + assertThatThrownBy(() -> extractDataFrom(event).as(Product.class)) .isInstanceOf(EventDeserializationException.class) .hasMessageContaining("consider using 'extractDataAsListOf' instead"); } @@ -111,7 +111,7 @@ public void testDeserializeSQSEventMessageAsObject_shouldThrowException(SQSEvent @Test public void testDeserializeProductAsProduct_shouldReturnProduct() { Product myProduct = new Product(1234, "product", 42); - Product product = from(myProduct).extractDataAs(Product.class); + Product product = extractDataFrom(myProduct).as(Product.class); assertProduct(product); } From 79c5568339ca60d755ad707edd753c0ad4cb7e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= Date: Fri, 18 Feb 2022 12:07:09 +0100 Subject: [PATCH 05/15] Update powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java Co-authored-by: Pankaj Agrawal --- .../lambda/powertools/utilities/EventDeserializerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index da4650baf..8d8ea4ecc 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -117,9 +117,9 @@ public void testDeserializeProductAsProduct_shouldReturnProduct() { private void assertProduct(Product product) { - assertThat(product.getId()).isEqualTo(1234); - assertThat(product.getName()).isEqualTo("product"); - assertThat(product.getPrice()).isEqualTo(42); +assertThat(product) + .isEqualTo(new Product(1234, "product", 42)) + .usingRecursiveComparison(); } } From 03d0ab632d5ca67d6459df317d606f0337470759 Mon Sep 17 00:00:00 2001 From: Pankaj Agrawal Date: Thu, 17 Feb 2022 10:58:17 +0100 Subject: [PATCH 06/15] chore: remove SQS and Idempotency examples (#754) --- example/HelloWorldFunction/build.gradle | 7 -- example/HelloWorldFunction/pom.xml | 41 --------- .../main/java/helloworld/AppIdempotency.java | 84 ------------------- .../src/main/java/helloworld/AppSqsEvent.java | 35 -------- .../main/java/helloworld/AppSqsEventUtil.java | 39 --------- example/template.yaml | 79 +---------------- 6 files changed, 1 insertion(+), 284 deletions(-) delete mode 100644 example/HelloWorldFunction/src/main/java/helloworld/AppIdempotency.java delete mode 100644 example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java delete mode 100644 example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java diff --git a/example/HelloWorldFunction/build.gradle b/example/HelloWorldFunction/build.gradle index b1a266606..18166b5f3 100644 --- a/example/HelloWorldFunction/build.gradle +++ b/example/HelloWorldFunction/build.gradle @@ -9,16 +9,9 @@ repositories { } dependencies { - aspect 'software.amazon.lambda:powertools-logging:1.11.0' - aspect 'software.amazon.lambda:powertools-tracing:1.11.0' - aspect 'software.amazon.lambda:powertools-metrics:1.11.0' - aspect 'software.amazon.lambda:powertools-sqs:1.11.0' aspect 'software.amazon.lambda:powertools-parameters:1.11.0' aspect 'software.amazon.lambda:powertools-validation:1.11.0' - implementation 'software.amazon.lambda:powertools-idempotency:1.11.0' - aspectpath 'software.amazon.lambda:powertools-idempotency:1.11.0' - implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml index 1d5b826a3..51a6a765d 100644 --- a/example/HelloWorldFunction/pom.xml +++ b/example/HelloWorldFunction/pom.xml @@ -13,21 +13,6 @@ - - software.amazon.lambda - powertools-tracing - 1.11.0 - - - software.amazon.lambda - powertools-logging - 1.11.0 - - - software.amazon.lambda - powertools-metrics - 1.11.0 - software.amazon.lambda powertools-parameters @@ -38,16 +23,6 @@ powertools-validation 1.11.0 - - software.amazon.lambda - powertools-sqs - 1.11.0 - - - software.amazon.lambda - powertools-idempotency - 1.11.0 - com.amazonaws aws-lambda-java-core @@ -93,22 +68,6 @@ ${maven.compiler.target} ${maven.compiler.target} - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-metrics - - - software.amazon.lambda - powertools-sqs - software.amazon.lambda powertools-validation diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppIdempotency.java b/example/HelloWorldFunction/src/main/java/helloworld/AppIdempotency.java deleted file mode 100644 index 64b5d79a5..000000000 --- a/example/HelloWorldFunction/src/main/java/helloworld/AppIdempotency.java +++ /dev/null @@ -1,84 +0,0 @@ -package helloworld; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.lambda.powertools.idempotency.Idempotency; -import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; -import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; -import software.amazon.lambda.powertools.utilities.JsonConfig; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -public class AppIdempotency implements RequestHandler { - private final static Logger LOG = LogManager.getLogger(); - - public AppIdempotency() { - // we need to initialize idempotency configuration before the handleRequest method is called - Idempotency.config().withConfig( - IdempotencyConfig.builder() - .withEventKeyJMESPath("powertools_json(body).address") - .build()) - .withPersistenceStore( - DynamoDBPersistenceStore.builder() - .withTableName("idempotency_table") - .build() - ).configure(); - } - - - /** - * Try with: - *
-     *     curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/helloidem/ -H "Content-Type: application/json" -d '{"address": "https://checkip.amazonaws.com"}'
-     * 
- * @param input - * @param context - * @return - */ - @Idempotent - public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - Map headers = new HashMap<>(); - - headers.put("Content-Type", "application/json"); - headers.put("Access-Control-Allow-Origin", "*"); - headers.put("Access-Control-Allow-Methods", "GET, OPTIONS"); - headers.put("Access-Control-Allow-Headers", "*"); - - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() - .withHeaders(headers); - try { - String address = JsonConfig.get().getObjectMapper().readTree(input.getBody()).get("address").asText(); - final String pageContents = this.getPageContents(address); - String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - - LOG.debug("ip is {}", pageContents); - return response - .withStatusCode(200) - .withBody(output); - - } catch (IOException e) { - return response - .withBody("{}") - .withStatusCode(500); - } - } - - // we could actually also put the @Idempotent annotation here - private String getPageContents(String address) throws IOException { - URL url = new URL(address); - try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { - return br.lines().collect(Collectors.joining(System.lineSeparator())); - } - } -} diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java b/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java deleted file mode 100644 index 31ba1f760..000000000 --- a/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -package helloworld; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; - -public class AppSqsEvent implements RequestHandler { - private static final Logger log = LogManager.getLogger(AppSqsEvent.class); - - @SqsBatch(SampleMessageHandler.class) - @Logging(logEvent = true) - @Override - public String handleRequest(SQSEvent input, Context context) { - return "{\"statusCode\": 200}"; - } - - public static class SampleMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if("19dd0b57-b21e-4ac1-bd88-01bbb068cb99".equals(message.getMessageId())) { - throw new RuntimeException(message.getMessageId()); - } - log.info("Processing message with details {}", message); - return message.getMessageId(); - } - } -} diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java b/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java deleted file mode 100644 index 3ececdfe1..000000000 --- a/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java +++ /dev/null @@ -1,39 +0,0 @@ -package helloworld; - -import java.util.List; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.lambda.powertools.sqs.SqsUtils; -import software.amazon.lambda.powertools.sqs.SQSBatchProcessingException; - -import static java.util.Collections.emptyList; - -public class AppSqsEventUtil implements RequestHandler> { - private static final Logger log = LogManager.getLogger(AppSqsEventUtil.class); - - @Override - public List handleRequest(SQSEvent input, Context context) { - try { - - return SqsUtils.batchProcessor(input, (message) -> { - if ("19dd0b57-b21e-4ac1-bd88-01bbb068cb99".equals(message.getMessageId())) { - throw new RuntimeException(message.getMessageId()); - } - - log.info("Processing message with details {}", message); - return message.getMessageId(); - }); - - } catch (SQSBatchProcessingException e) { - log.info("Exception details {}", e.getMessage(), e); - log.info("Success message Returns{}", e.successMessageReturnValues()); - log.info("Failed messages {}", e.getFailures()); - log.info("Failed messages Reasons {}", e.getExceptions()); - return emptyList(); - } - } -} diff --git a/example/template.yaml b/example/template.yaml index 901d8306d..907ae13b1 100644 --- a/example/template.yaml +++ b/example/template.yaml @@ -61,27 +61,6 @@ Resources: Name: id Type: String - HelloWorldIdempotentFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: HelloWorldFunction - Handler: helloworld.AppIdempotency::handleRequest - MemorySize: 512 - Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object - Variables: - POWERTOOLS_LOG_LEVEL: INFO - AWS_ENDPOINT_DISCOVERY_ENABLED: false - Tracing: Active - Policies: - - DynamoDBCrudPolicy: - TableName: !Ref IdempotencyTable - Events: - HelloWorld: - Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api - Properties: - Path: /helloidem - Method: post - UserPwd: Type: AWS::SecretsManager::Secret Properties: @@ -131,55 +110,6 @@ Resources: Value: aGVsbG8gd29ybGQ= Description: Base64 SSM Parameter for lambda-powertools-java powertools-parameters module - TestSqsQueue: - Type: AWS::SQS::Queue - - HelloWorldSqsEventFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: HelloWorldFunction - Handler: helloworld.AppSqsEvent::handleRequest - MemorySize: 512 - Tracing: Active - Policies: - - Statement: - - Sid: AdditionalPermisssionForPowertoolsSQSUtils - Effect: Allow - Action: - - sqs:GetQueueUrl - - sqs:DeleteMessageBatch - Resource: !GetAtt TestSqsQueue.Arn - Events: - TestSQSEvent: - Type: SQS - Properties: - Queue: !GetAtt TestSqsQueue.Arn - BatchSize: 10 - - TestAnotherSqsQueue: - Type: AWS::SQS::Queue - - HelloWorldSqsEventUtilFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: HelloWorldFunction - Handler: helloworld.AppSqsEventUtil::handleRequest - MemorySize: 512 - Tracing: Active - Policies: - - Statement: - - Sid: AdditionalPermisssionForPowertoolsSQSUtils - Effect: Allow - Action: - - sqs:GetQueueUrl - - sqs:DeleteMessageBatch - Resource: !GetAtt TestAnotherSqsQueue.Arn - Events: - TestSQSEvent: - Type: SQS - Properties: - Queue: !GetAtt TestAnotherSqsQueue.Arn - BatchSize: 10 Outputs: # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function @@ -198,11 +128,4 @@ Outputs: Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/helloparams/" HelloWorldParamsFunction: Description: "Hello World Params Lambda Function ARN" - Value: !GetAtt HelloWorldParamsFunction.Arn - - HelloWorldIdempotencyApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World idempotency function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/helloidem/" - HelloWorldIdempotencyFunction: - Description: "Hello World Idempotency Lambda Function ARN" - Value: !GetAtt HelloWorldIdempotentFunction.Arn + Value: !GetAtt HelloWorldParamsFunction.Arn \ No newline at end of file From cf6cd3eeaad824d8a779c65e748f173fba26ffea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Feb 2022 12:11:38 +0100 Subject: [PATCH 07/15] build(deps): bump maven-jar-plugin from 3.2.0 to 3.2.2 (#756) Bumps [maven-jar-plugin](https://github.com/apache/maven-jar-plugin) from 3.2.0 to 3.2.2. - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.2.0...maven-jar-plugin-3.2.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- powertools-idempotency/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml index 7db7ed3a4..d067c9f96 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -170,7 +170,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.2.0 + 3.2.2 From 17b456b7fedaa22b3c741dca9bee0757cbd50e69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Feb 2022 12:11:59 +0100 Subject: [PATCH 08/15] build(deps): bump aws.sdk.version from 2.17.130 to 2.17.131 (#755) Bumps `aws.sdk.version` from 2.17.130 to 2.17.131. Updates `software.amazon.awssdk:bom` from 2.17.130 to 2.17.131 - [Release notes](https://github.com/aws/aws-sdk-java-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-java-v2/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java-v2/compare/2.17.130...2.17.131) Updates `http-client-spi` from 2.17.130 to 2.17.131 - [Release notes](https://github.com/aws/aws-sdk-java-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-java-v2/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java-v2/compare/2.17.130...2.17.131) Updates `url-connection-client` from 2.17.130 to 2.17.131 --- updated-dependencies: - dependency-name: software.amazon.awssdk:bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:http-client-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: software.amazon.awssdk:url-connection-client dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ac2d84a4f..32a9c9ee4 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ 2.17.1 2.13.1 1.9.7 - 2.17.130 + 2.17.131 2.11.0 1.1.1 UTF-8 From 09df58af2358f5a235cfa45b63d8d19bc81d73b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= Date: Fri, 18 Feb 2022 05:29:19 +0100 Subject: [PATCH 09/15] fix #758 (#759) remove trailing slash for multiple params --- .../powertools/parameters/BaseProvider.java | 10 ++++---- .../parameters/SSMProviderTest.java | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java index 706b555c1..fb539f850 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java @@ -123,13 +123,15 @@ public BaseProvider withTransformation(Class transformerC */ @Override public Map getMultiple(String path) { + // remove trailing whitespace + String pathWithoutTrailingSlash = path.replaceAll("\\/+$", ""); try { - return (Map) cacheManager.getIfNotExpired(path, now()).orElseGet(() -> { - Map params = getMultipleValues(path); + return (Map) cacheManager.getIfNotExpired(pathWithoutTrailingSlash, now()).orElseGet(() -> { + Map params = getMultipleValues(pathWithoutTrailingSlash); - cacheManager.putInCache(path, params); + cacheManager.putInCache(pathWithoutTrailingSlash, params); - params.forEach((k, v) -> cacheManager.putInCache(path + "/" + k, v)); + params.forEach((k, v) -> cacheManager.putInCache(pathWithoutTrailingSlash + "/" + k, v)); return params; }); diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java index 761979e00..da299c38d 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java +++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java @@ -103,6 +103,29 @@ public void getMultiple() { assertThat(paramByPathCaptor.getValue().recursive()).isFalse(); } + @Test + public void getMultipleWithTrailingSlash() { + List parameters = new ArrayList<>(); + parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build()); + parameters.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build()); + parameters.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build()); + GetParametersByPathResponse response = GetParametersByPathResponse.builder().parameters(parameters).build(); + when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response); + + Map params = provider.getMultiple("/prod/app1/"); + assertThat(params).contains( + MapEntry.entry("key1", "foo1"), + MapEntry.entry("key2", "foo2"), + MapEntry.entry("key3", "foo3")); + assertThat(provider.get("/prod/app1/key1")).isEqualTo("foo1"); + assertThat(provider.get("/prod/app1/key2")).isEqualTo("foo2"); + assertThat(provider.get("/prod/app1/key3")).isEqualTo("foo3"); + + assertThat(paramByPathCaptor.getValue().path()).isEqualTo("/prod/app1"); + assertThat(paramByPathCaptor.getValue().withDecryption()).isFalse(); + assertThat(paramByPathCaptor.getValue().recursive()).isFalse(); + } + @Test public void getMultiple_cached_shouldNotCallSSM() { List parameters = new ArrayList<>(); From 3a06d1b9ffe693dd0c86fdd71cbd7d338cf9db95 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Fri, 18 Feb 2022 13:38:35 +0100 Subject: [PATCH 10/15] changes post review: - javadoc - indentation - better edge cases handling --- .../utilities/EventDeserializer.java | 176 ++++++++++++------ 1 file changed, 123 insertions(+), 53 deletions(-) diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java index 3bf92b55b..4b7aeb416 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java @@ -26,90 +26,149 @@ import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress; +/** + * Class that can be used to extract the meaningful part of an event and deserialize it into a Java object.
+ * For example, extract the body of an API Gateway event, or messages from an SQS event. + */ public class EventDeserializer { private static final Logger LOG = LoggerFactory.getLogger(EventDeserializer.class); - public static EventPart extractDataFrom(Object obj) { - if (obj instanceof String) { - return new EventPart((String) obj); - } else if (obj instanceof Map) { - return new EventPart((Map) obj); - } else if (obj instanceof APIGatewayProxyRequestEvent) { - APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; + /** + * Extract the meaningful part of a Lambda Event object. Main events are built-in: + *
    + *
  • {@link APIGatewayProxyRequestEvent} -> body
  • + *
  • {@link APIGatewayV2HTTPEvent} -> body
  • + *
  • {@link SNSEvent} -> Records[0].Sns.Message
  • + *
  • {@link SQSEvent} -> Records[*].body (list)
  • + *
  • {@link ScheduledEvent} -> detail
  • + *
  • {@link ApplicationLoadBalancerRequestEvent} -> body
  • + *
  • {@link CloudWatchLogsEvent} -> powertools_base64_gzip(data)
  • + *
  • {@link CloudFormationCustomResourceEvent} -> resourceProperties
  • + *
  • {@link KinesisEvent} -> Records[*].kinesis.powertools_base64(data) (list)
  • + *
  • {@link KinesisFirehoseEvent} -> Records[*].powertools_base64(data) (list)
  • + *
  • {@link KafkaEvent} -> records[*].values[*].powertools_base64(value) (list)
  • + *
  • {@link ActiveMQEvent} -> messages[*].powertools_base64(data) (list)
  • + *
  • {@link RabbitMQEvent} -> rmqMessagesByQueue[*].values[*].powertools_base64(data) (list)
  • + *
  • {@link KinesisAnalyticsFirehoseInputPreprocessingEvent} -> Records[*].kinesis.powertools_base64(data) (list)
  • + *
  • {@link KinesisAnalyticsStreamsInputPreprocessingEvent} > Records[*].kinesis.powertools_base64(data) (list)
  • + *
  • {@link String}
  • + *
  • {@link Map}
  • + *
+ * To be used in conjunction with {@link EventPart#as(Class)} or {@link EventPart#asListOf(Class)} + * for the deserialization. + * + * @param object the event of your Lambda function handler method + * @return the part of the event which is meaningful (ex: body of the API Gateway).
+ */ + public static EventPart extractDataFrom(Object object) { + if (object instanceof String) { + return new EventPart((String) object); + } else if (object instanceof Map) { + return new EventPart((Map) object); + } else if (object instanceof APIGatewayProxyRequestEvent) { + APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) object; return new EventPart(event.getBody()); - } else if (obj instanceof APIGatewayV2HTTPEvent) { - APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; + } else if (object instanceof APIGatewayV2HTTPEvent) { + APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) object; return new EventPart(event.getBody()); - } else if (obj instanceof SNSEvent) { - SNSEvent event = (SNSEvent) obj; + } else if (object instanceof SNSEvent) { + SNSEvent event = (SNSEvent) object; return new EventPart(event.getRecords().get(0).getSNS().getMessage()); - } else if (obj instanceof SQSEvent) { - SQSEvent event = (SQSEvent) obj; - return new EventPart(event.getRecords().stream().map(SQSEvent.SQSMessage::getBody).collect(Collectors.toList())); - } else if (obj instanceof ScheduledEvent) { - ScheduledEvent event = (ScheduledEvent) obj; + } else if (object instanceof SQSEvent) { + SQSEvent event = (SQSEvent) object; + return new EventPart(event.getRecords().stream() + .map(SQSEvent.SQSMessage::getBody) + .collect(Collectors.toList())); + } else if (object instanceof ScheduledEvent) { + ScheduledEvent event = (ScheduledEvent) object; return new EventPart(event.getDetail()); - } else if (obj instanceof ApplicationLoadBalancerRequestEvent) { - ApplicationLoadBalancerRequestEvent event = (ApplicationLoadBalancerRequestEvent) obj; + } else if (object instanceof ApplicationLoadBalancerRequestEvent) { + ApplicationLoadBalancerRequestEvent event = (ApplicationLoadBalancerRequestEvent) object; return new EventPart(event.getBody()); - } else if (obj instanceof CloudWatchLogsEvent) { - CloudWatchLogsEvent event = (CloudWatchLogsEvent) obj; + } else if (object instanceof CloudWatchLogsEvent) { + CloudWatchLogsEvent event = (CloudWatchLogsEvent) object; return new EventPart(decompress(decode(event.getAwsLogs().getData().getBytes(UTF_8)))); - } else if (obj instanceof CloudFormationCustomResourceEvent) { - CloudFormationCustomResourceEvent event = (CloudFormationCustomResourceEvent) obj; + } else if (object instanceof CloudFormationCustomResourceEvent) { + CloudFormationCustomResourceEvent event = (CloudFormationCustomResourceEvent) object; return new EventPart(event.getResourceProperties()); - } else if (obj instanceof KinesisEvent) { - KinesisEvent event = (KinesisEvent) obj; - return new EventPart(event.getRecords().stream().map(r -> decode(r.getKinesis().getData())).collect(Collectors.toList())); - } else if (obj instanceof KinesisFirehoseEvent) { - KinesisFirehoseEvent event = (KinesisFirehoseEvent) obj; - return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList())); - } else if (obj instanceof KafkaEvent) { - KafkaEvent event = (KafkaEvent) obj; - return new EventPart(event.getRecords().values().stream().flatMap(List::stream).map(r -> decode(r.getValue())).collect(Collectors.toList())); - } else if (obj instanceof ActiveMQEvent) { - ActiveMQEvent event = (ActiveMQEvent) obj; - return new EventPart(event.getMessages().stream().map(m -> decode(m.getData())).collect(Collectors.toList())); - } else if (obj instanceof RabbitMQEvent) { - RabbitMQEvent event = (RabbitMQEvent) obj; - return new EventPart(event.getRmqMessagesByQueue().values().stream().flatMap(List::stream).map(r -> decode(r.getData())).collect(Collectors.toList())); - } else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { - KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj; - return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList())); - } else if (obj instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { - KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) obj; - return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList())); + } else if (object instanceof KinesisEvent) { + KinesisEvent event = (KinesisEvent) object; + return new EventPart(event.getRecords().stream() + .map(r -> decode(r.getKinesis().getData())) + .collect(Collectors.toList())); + } else if (object instanceof KinesisFirehoseEvent) { + KinesisFirehoseEvent event = (KinesisFirehoseEvent) object; + return new EventPart(event.getRecords().stream() + .map(r -> decode(r.getData())) + .collect(Collectors.toList())); + } else if (object instanceof KafkaEvent) { + KafkaEvent event = (KafkaEvent) object; + return new EventPart(event.getRecords().values().stream() + .flatMap(List::stream) + .map(r -> decode(r.getValue())) + .collect(Collectors.toList())); + } else if (object instanceof ActiveMQEvent) { + ActiveMQEvent event = (ActiveMQEvent) object; + return new EventPart(event.getMessages().stream() + .map(m -> decode(m.getData())) + .collect(Collectors.toList())); + } else if (object instanceof RabbitMQEvent) { + RabbitMQEvent event = (RabbitMQEvent) object; + return new EventPart(event.getRmqMessagesByQueue().values().stream() + .flatMap(List::stream) + .map(r -> decode(r.getData())) + .collect(Collectors.toList())); + } else if (object instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { + KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) object; + return new EventPart(event.getRecords().stream() + .map(r -> decode(r.getData())) + .collect(Collectors.toList())); + } else if (object instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { + KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) object; + return new EventPart(event.getRecords().stream() + .map(r -> decode(r.getData())) + .collect(Collectors.toList())); } else { - // does not really make sense to use this EventLoader when you already have a typed object + // does not really make sense to use this EventDeserializer when you already have a typed object // just not to throw an exception LOG.warn("Consider using your object directly instead of using EventDeserializer"); - return new EventPart(obj); + return new EventPart(object); } } + /** + * Meaningful part of a Lambda event.
+ * Use {@link #extractDataFrom(Object)} to retrieve an instance of this class. + */ public static class EventPart { private Map contentMap; private String content; private List contentList; private Object contentObject; - public EventPart(List contentList) { + private EventPart(List contentList) { this.contentList = contentList; } - public EventPart(String content) { + private EventPart(String content) { this.content = content; } - public EventPart(Map contentMap) { + private EventPart(Map contentMap) { this.contentMap = contentMap; } - public EventPart(Object content) { + private EventPart(Object content) { this.contentObject = content; } + /** + * Deserialize this part of event from JSON to an object of type T + * @param clazz the target type for deserialization + * @param type of object to return + * @return an Object of type T (deserialized from the content) + */ public T as(Class clazz) { try { if (content != null) { @@ -126,21 +185,32 @@ public T as(Class clazz) { return (T) contentObject; } if (contentList != null) { - throw new EventDeserializationException("The content of this event is a list, consider using 'extractDataAsListOf' instead"); + throw new EventDeserializationException("The content of this event is a list, consider using 'asListOf' instead"); } - throw new EventDeserializationException("Event content is null"); + // should not occur, except if the event is malformed (missing fields) + throw new IllegalStateException("Event content is null"); } catch (IOException e) { throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e); } } + /** + * Deserialize this part of event from JSON to a list of objects of type T + * @param clazz the target type for deserialization + * @param type of object to return + * @return a list of objects of type T (deserialized from the content) + */ public List asListOf(Class clazz) { if (contentList == null) { - throw new EventDeserializationException("Event content is null"); + if (content != null || contentMap != null || contentObject != null) { + throw new EventDeserializationException("The content of this event is not a list, consider using 'as' instead"); + } + // should not occur, except if the event is really malformed + throw new IllegalStateException("Event content is null"); } return contentList.stream().map(s -> { try { - return JsonConfig.get().getObjectMapper().reader().readValue(s, clazz); + return s == null ? null : JsonConfig.get().getObjectMapper().reader().readValue(s, clazz); } catch (IOException e) { throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e); } From 56840f4a13623a86e07251f3b1d5291adee3dc1b Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Fri, 18 Feb 2022 13:38:58 +0100 Subject: [PATCH 11/15] add tests for edge cases --- .../utilities/EventDeserializerTest.java | 33 +++++++++- .../test/resources/apigw_event_no_body.json | 61 +++++++++++++++++++ .../src/test/resources/sqs_event_no_body.json | 21 +++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 powertools-serialization/src/test/resources/apigw_event_no_body.json create mode 100644 powertools-serialization/src/test/resources/sqs_event_no_body.json diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index 8d8ea4ecc..fb532a149 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -105,7 +105,38 @@ public void testDeserializeKafkaEventMessageAsList_shouldReturnList(KafkaEvent e public void testDeserializeSQSEventMessageAsObject_shouldThrowException(SQSEvent event) { assertThatThrownBy(() -> extractDataFrom(event).as(Product.class)) .isInstanceOf(EventDeserializationException.class) - .hasMessageContaining("consider using 'extractDataAsListOf' instead"); + .hasMessageContaining("consider using 'asListOf' instead"); + } + + @ParameterizedTest + @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) + public void testDeserializeAPIGatewayEventAsList_shouldThrowException(APIGatewayProxyRequestEvent event) { + assertThatThrownBy(() -> extractDataFrom(event).asListOf(Product.class)) + .isInstanceOf(EventDeserializationException.class) + .hasMessageContaining("consider using 'as' instead"); + } + + @ParameterizedTest + @Event(value = "sqs_event.json", type = SQSEvent.class) + public void testDeserializeSQSEventBodyAsWrongObjectType_shouldThrowException(SQSEvent event) { + assertThatThrownBy(() -> extractDataFrom(event).asListOf(Basket.class)) + .isInstanceOf(EventDeserializationException.class) + .hasMessage("Cannot load the event as Basket"); + } + + @ParameterizedTest + @Event(value = "apigw_event_no_body.json", type = APIGatewayProxyRequestEvent.class) + public void testDeserializeAPIGatewayNoBody_shouldThrowException(APIGatewayProxyRequestEvent event) { + assertThatThrownBy(() -> extractDataFrom(event).as(Product.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Event content is null"); + } + + @ParameterizedTest + @Event(value = "sqs_event_no_body.json", type = SQSEvent.class) + public void testDeserializeSQSEventNoBody_shouldThrowException(SQSEvent event) { + List products = extractDataFrom(event).asListOf(Product.class); + assertThat(products.get(0)).isNull(); } @Test diff --git a/powertools-serialization/src/test/resources/apigw_event_no_body.json b/powertools-serialization/src/test/resources/apigw_event_no_body.json new file mode 100644 index 000000000..f534c91a3 --- /dev/null +++ b/powertools-serialization/src/test/resources/apigw_event_no_body.json @@ -0,0 +1,61 @@ +{ + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/powertools-serialization/src/test/resources/sqs_event_no_body.json b/powertools-serialization/src/test/resources/sqs_event_no_body.json new file mode 100644 index 000000000..3a313dd6b --- /dev/null +++ b/powertools-serialization/src/test/resources/sqs_event_no_body.json @@ -0,0 +1,21 @@ +{ + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file From 379075fdd2c58be306f4aa2496c00765712075f7 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Mon, 28 Feb 2022 10:18:15 +0100 Subject: [PATCH 12/15] exception messages detailed --- .../amazon/lambda/powertools/utilities/EventDeserializer.java | 4 ++-- .../lambda/powertools/utilities/EventDeserializerTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java index 4b7aeb416..0c120576c 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java @@ -188,7 +188,7 @@ public T as(Class clazz) { throw new EventDeserializationException("The content of this event is a list, consider using 'asListOf' instead"); } // should not occur, except if the event is malformed (missing fields) - throw new IllegalStateException("Event content is null"); + throw new IllegalStateException("Event content is null: the event may be malformed (missing fields)"); } catch (IOException e) { throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e); } @@ -206,7 +206,7 @@ public List asListOf(Class clazz) { throw new EventDeserializationException("The content of this event is not a list, consider using 'as' instead"); } // should not occur, except if the event is really malformed - throw new IllegalStateException("Event content is null"); + throw new IllegalStateException("Event content is null: the event may be malformed (missing fields)"); } return contentList.stream().map(s -> { try { diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index fb532a149..ce2e5621a 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -129,7 +129,7 @@ public void testDeserializeSQSEventBodyAsWrongObjectType_shouldThrowException(SQ public void testDeserializeAPIGatewayNoBody_shouldThrowException(APIGatewayProxyRequestEvent event) { assertThatThrownBy(() -> extractDataFrom(event).as(Product.class)) .isInstanceOf(IllegalStateException.class) - .hasMessage("Event content is null"); + .hasMessageContaining("Event content is null"); } @ParameterizedTest From 1c237382f8d101510ad96c1642bb22dade2cb3d4 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Mon, 28 Feb 2022 13:44:54 +0100 Subject: [PATCH 13/15] deserialize a string as list --- .../utilities/EventDeserializer.java | 25 +++++++++++++------ .../utilities/EventDeserializerTest.java | 18 ++++++++++++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java index 0c120576c..9742299ee 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java @@ -14,6 +14,8 @@ package software.amazon.lambda.powertools.utilities; import com.amazonaws.services.lambda.runtime.events.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -201,20 +203,29 @@ public T as(Class clazz) { * @return a list of objects of type T (deserialized from the content) */ public List asListOf(Class clazz) { - if (contentList == null) { - if (content != null || contentMap != null || contentObject != null) { + if (contentList == null && content == null) { + if (contentMap != null || contentObject != null) { throw new EventDeserializationException("The content of this event is not a list, consider using 'as' instead"); } // should not occur, except if the event is really malformed throw new IllegalStateException("Event content is null: the event may be malformed (missing fields)"); } - return contentList.stream().map(s -> { + if (content != null) { + ObjectReader reader = JsonConfig.get().getObjectMapper().readerForListOf(clazz); try { - return s == null ? null : JsonConfig.get().getObjectMapper().reader().readValue(s, clazz); - } catch (IOException e) { - throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e); + return reader.readValue(content); + } catch (JsonProcessingException e) { + throw new EventDeserializationException("Cannot load the event as a list of " + clazz.getSimpleName() + ", consider using 'as' instead", e); } - }).collect(Collectors.toList()); + } else { + return contentList.stream().map(s -> { + try { + return s == null ? null : JsonConfig.get().getObjectMapper().reader().readValue(s, clazz); + } catch (IOException e) { + throw new EventDeserializationException("Cannot load the event as a list of " + clazz.getSimpleName(), e); + } + }).collect(Collectors.toList()); + } } } } diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index ce2e5621a..90143b2a0 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -44,6 +44,22 @@ public void testDeserializeStringAsObject_shouldReturnObject() { assertProduct(product); } + @Test + public void testDeserializeStringArrayAsList_shouldReturnList() { + String productStr = "[{\"id\":1234, \"name\":\"product\", \"price\":42}, {\"id\":2345, \"name\":\"product2\", \"price\":43}]"; + List products = extractDataFrom(productStr).asListOf(Product.class); + assertThat(products).hasSize(2); + assertProduct(products.get(0)); + } + + @Test + public void testDeserializeStringAsList_shouldThrowException() { + String productStr = "{\"id\":1234, \"name\":\"product\", \"price\":42}"; + assertThatThrownBy(() -> extractDataFrom(productStr).asListOf(Product.class)) + .isInstanceOf(EventDeserializationException.class) + .hasMessage("Cannot load the event as a list of Product, consider using 'as' instead"); + } + @Test public void testDeserializeMapAsObject_shouldReturnObject() { Map map = new HashMap<>(); @@ -121,7 +137,7 @@ public void testDeserializeAPIGatewayEventAsList_shouldThrowException(APIGateway public void testDeserializeSQSEventBodyAsWrongObjectType_shouldThrowException(SQSEvent event) { assertThatThrownBy(() -> extractDataFrom(event).asListOf(Basket.class)) .isInstanceOf(EventDeserializationException.class) - .hasMessage("Cannot load the event as Basket"); + .hasMessage("Cannot load the event as a list of Basket"); } @ParameterizedTest From 13e5269f23d03234ebe7d35ca0c56c08d876ec6c Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Mon, 28 Feb 2022 13:45:30 +0100 Subject: [PATCH 14/15] documentation --- docs/utilities/serialization.md | 239 +++++++++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 1 deletion(-) diff --git a/docs/utilities/serialization.md b/docs/utilities/serialization.md index 19ff00d37..72aa1c2ab 100644 --- a/docs/utilities/serialization.md +++ b/docs/utilities/serialization.md @@ -3,7 +3,244 @@ title: Serialization Utilities description: Utility --- -This module contains a set of utilities you may use in your Lambda functions, mainly associated with other modules like [validation](validation.md) and [idempotency](idempotency.md), to manipulate JSON. +This module contains a set of utilities you may use in your Lambda functions, to manipulate JSON. + +## Easy deserialization + +### Key features + +* Easily deserialize the main content of an event (for example, the body of an API Gateway event) +* 15+ built-in events (see the [list below](#built-in-events)) + +### Getting started + + +=== "Maven" + + ```xml hl_lines="5" + + ... + + software.amazon.lambda + powertools-serialization + {{ powertools.version }} + + ... + + ``` + +### EventDeserializer + +The `EventDeserializer` can be used to extract the main part of an event (body, message, records) and deserialize it from JSON to your desired type. + +It can handle single elements like the body of an API Gateway event: + +=== "APIGWHandler.java" + + ```java hl_lines="1 6 9" + import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; + + public class APIGWHandler implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest( + final APIGatewayProxyRequestEvent event, + final Context context) { + + Product product = extractDataFrom(event).as(Product.class); + + } + ``` + +=== "Product.java" + + ```java + public class Product { + private long id; + private String name; + private double price; + + public Product() { + } + + public Product(long id, String name, double price) { + this.id = id; + this.name = name; + this.price = price; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + } + ``` + +=== "event" + + ```json hl_lines="2" + { + "body": "{\"id\":1234, \"name\":\"product\", \"price\":42}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + } + ``` + +It can also handle a collection of elements like the records of an SQS event: + +=== "SQSHandler.java" + + ```java hl_lines="1 6 9" + import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; + + public class SQSHandler implements RequestHandler { + + public String handleRequest( + final SQSEvent event, + final Context context) { + + List products = extractDataFrom(event).asListOf(Product.class); + + } + ``` + +=== "event" + + ```json hl_lines="6 23" + { + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{ \"id\": 1234, \"name\": \"product\", \"price\": 42}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{ \"id\": 12345, \"name\": \"product5\", \"price\": 45}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] + } + ``` + +!!! Tip + In the background, `EventDeserializer` is using Jackson. The `ObjectMapper` is configured in `JsonConfig`. You can customize the configuration of the mapper if needed: + `JsonConfig.get().getObjectMapper()`. Using this feature, you don't need to add Jackson to your project and create another instance of `ObjectMapper`. + +### Built-in events + +| Event Type | Path to the content | List | +|---------------------------------------------------|-----------------------------------------------------------|------| +| `APIGatewayProxyRequestEvent` | `body` | | +| `APIGatewayV2HTTPEvent` | `body` | | +| `SNSEvent` | `Records[0].Sns.Message` | | +| `SQSEvent` | `Records[*].body` | x | + | `ScheduledEvent` | `detail` | | + | `ApplicationLoadBalancerRequestEvent` | `body` | | + | `CloudWatchLogsEvent` | `powertools_base64_gzip(data)` | | + | `CloudFormationCustomResourceEvent` | `resourceProperties` | | + | `KinesisEvent` | `Records[*].kinesis.powertools_base64(data)` | x | + | `KinesisFirehoseEvent` | `Records[*].powertools_base64(data)` | x | + | `KafkaEvent` | `records[*].values[*].powertools_base64(value)` | x | + | `ActiveMQEvent` | `messages[*].powertools_base64(data)` | x | +| `RabbitMQEvent` | `rmqMessagesByQueue[*].values[*].powertools_base64(data)` | x | +| `KinesisAnalyticsFirehoseInputPreprocessingEvent` | `Records[*].kinesis.powertools_base64(data)` | x | +| `KinesisAnalyticsStreamsInputPreprocessingEvent` | `Records[*].kinesis.powertools_base64(data)` | x | + ## JMESPath functions From db08f0d4d1b68036e44b7c17f611d773c66c202e Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Tue, 1 Mar 2022 10:08:15 +0100 Subject: [PATCH 15/15] gradle example --- docs/utilities/serialization.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/utilities/serialization.md b/docs/utilities/serialization.md index 72aa1c2ab..d9e392ca4 100644 --- a/docs/utilities/serialization.md +++ b/docs/utilities/serialization.md @@ -14,7 +14,6 @@ This module contains a set of utilities you may use in your Lambda functions, to ### Getting started - === "Maven" ```xml hl_lines="5" @@ -29,6 +28,12 @@ This module contains a set of utilities you may use in your Lambda functions, to ``` +=== "Gradle" + + ``` + implementation 'software.amazon.lambda:powertools-serialization:{{ powertools.version }}' + ``` + ### EventDeserializer The `EventDeserializer` can be used to extract the main part of an event (body, message, records) and deserialize it from JSON to your desired type.