diff --git a/docs/utilities/serialization.md b/docs/utilities/serialization.md index 19ff00d37..d9e392ca4 100644 --- a/docs/utilities/serialization.md +++ b/docs/utilities/serialization.md @@ -3,7 +3,249 @@ 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 }} + + ... + + ``` + +=== "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. + +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 diff --git a/pom.xml b/pom.xml index 50f935840..28e1f941d 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/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..9742299ee --- /dev/null +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/EventDeserializer.java @@ -0,0 +1,231 @@ +/* + * 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.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectReader; +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; + +/** + * 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); + + /** + * 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 (object instanceof APIGatewayV2HTTPEvent) { + APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) object; + return new EventPart(event.getBody()); + } else if (object instanceof SNSEvent) { + SNSEvent event = (SNSEvent) object; + return new EventPart(event.getRecords().get(0).getSNS().getMessage()); + } 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 (object instanceof ApplicationLoadBalancerRequestEvent) { + ApplicationLoadBalancerRequestEvent event = (ApplicationLoadBalancerRequestEvent) object; + return new EventPart(event.getBody()); + } else if (object instanceof CloudWatchLogsEvent) { + CloudWatchLogsEvent event = (CloudWatchLogsEvent) object; + return new EventPart(decompress(decode(event.getAwsLogs().getData().getBytes(UTF_8)))); + } else if (object instanceof CloudFormationCustomResourceEvent) { + CloudFormationCustomResourceEvent event = (CloudFormationCustomResourceEvent) object; + return new EventPart(event.getResourceProperties()); + } 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 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(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; + + private EventPart(List contentList) { + this.contentList = contentList; + } + + private EventPart(String content) { + this.content = content; + } + + private EventPart(Map contentMap) { + this.contentMap = contentMap; + } + + 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) { + 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 'asListOf' instead"); + } + // should not occur, except if the event is malformed (missing fields) + 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); + } + } + + /** + * 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 && 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)"); + } + if (content != null) { + ObjectReader reader = JsonConfig.get().getObjectMapper().readerForListOf(clazz); + try { + 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); + } + } 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/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-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..90143b2a0 --- /dev/null +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -0,0 +1,172 @@ +/* + * 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.extractDataFrom; + +public class EventDeserializerTest { + + @Test + public void testDeserializeStringAsString_shouldReturnString() { + String stringEvent = "Hello World"; + 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 = extractDataFrom(productStr).as(Product.class); + 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<>(); + map.put("id", 1234); + map.put("name", "product"); + map.put("price", 42); + 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 = extractDataFrom(event).as(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) + public void testDeserializeAPIGWEventBodyAsWrongObjectType_shouldThrowException(APIGatewayProxyRequestEvent event) { + assertThatThrownBy(() -> extractDataFrom(event).as(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 = extractDataFrom(event).as(Product.class); + assertProduct(product); + } + + @ParameterizedTest + @Event(value = "sqs_event.json", type = SQSEvent.class) + public void testDeserializeSQSEventMessageAsList_shouldReturnList(SQSEvent event) { + List products = extractDataFrom(event).asListOf(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 = extractDataFrom(event).asListOf(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 = extractDataFrom(event).asListOf(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(() -> extractDataFrom(event).as(Product.class)) + .isInstanceOf(EventDeserializationException.class) + .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 a list of 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) + .hasMessageContaining("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 + public void testDeserializeProductAsProduct_shouldReturnProduct() { + Product myProduct = new Product(1234, "product", 42); + Product product = extractDataFrom(myProduct).as(Product.class); + assertProduct(product); + } + + + private void assertProduct(Product product) { +assertThat(product) + .isEqualTo(new Product(1234, "product", 42)) + .usingRecursiveComparison(); + } + +} 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/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/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/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 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/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) { 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==", 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 @@ + + + + + + + +