Skip to content

Commit c5e04d9

Browse files
committed
feature: easy deserialization of event content
1 parent 7008023 commit c5e04d9

File tree

12 files changed

+641
-0
lines changed

12 files changed

+641
-0
lines changed

powertools-serialization/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
<groupId>io.burt</groupId>
4646
<artifactId>jmespath-jackson</artifactId>
4747
</dependency>
48+
<dependency>
49+
<groupId>com.amazonaws</groupId>
50+
<artifactId>aws-lambda-java-events</artifactId>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.apache.logging.log4j</groupId>
54+
<artifactId>log4j-slf4j-impl</artifactId>
55+
</dependency>
4856

4957
<!-- Test dependencies -->
5058
<dependency>
@@ -57,6 +65,11 @@
5765
<artifactId>assertj-core</artifactId>
5866
<scope>test</scope>
5967
</dependency>
68+
<dependency>
69+
<groupId>com.amazonaws</groupId>
70+
<artifactId>aws-lambda-java-tests</artifactId>
71+
<scope>test</scope>
72+
</dependency>
6073
</dependencies>
6174

6275
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
package software.amazon.lambda.powertools.utilities;
15+
16+
public class EventDeserializationException extends RuntimeException {
17+
private static final long serialVersionUID = -5003158148870110442L;
18+
19+
public EventDeserializationException(String msg, Exception e) {
20+
super(msg, e);
21+
}
22+
23+
public EventDeserializationException(String msg) {
24+
super(msg);
25+
}
26+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
package software.amazon.lambda.powertools.utilities;
15+
16+
import com.amazonaws.services.lambda.runtime.events.*;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
import java.io.IOException;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.stream.Collectors;
24+
25+
import static java.nio.charset.StandardCharsets.UTF_8;
26+
import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode;
27+
import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress;
28+
29+
public class EventDeserializer {
30+
31+
private static final Logger LOG = LoggerFactory.getLogger(EventDeserializer.class);
32+
33+
public static EventPart from(Object obj) {
34+
if (obj instanceof String) {
35+
return new EventPart((String) obj);
36+
} else if (obj instanceof Map) {
37+
return new EventPart((Map<String, Object>) obj);
38+
} else if (obj instanceof APIGatewayProxyRequestEvent) {
39+
APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj;
40+
return new EventPart(event.getBody());
41+
} else if (obj instanceof APIGatewayV2HTTPEvent) {
42+
APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj;
43+
return new EventPart(event.getBody());
44+
} else if (obj instanceof SNSEvent) {
45+
SNSEvent event = (SNSEvent) obj;
46+
return new EventPart(event.getRecords().get(0).getSNS().getMessage());
47+
} else if (obj instanceof SQSEvent) {
48+
SQSEvent event = (SQSEvent) obj;
49+
return new EventPart(event.getRecords().stream().map(SQSEvent.SQSMessage::getBody).collect(Collectors.toList()));
50+
} else if (obj instanceof ScheduledEvent) {
51+
ScheduledEvent event = (ScheduledEvent) obj;
52+
return new EventPart(event.getDetail());
53+
} else if (obj instanceof ApplicationLoadBalancerRequestEvent) {
54+
ApplicationLoadBalancerRequestEvent event = (ApplicationLoadBalancerRequestEvent) obj;
55+
return new EventPart(event.getBody());
56+
} else if (obj instanceof CloudWatchLogsEvent) {
57+
CloudWatchLogsEvent event = (CloudWatchLogsEvent) obj;
58+
return new EventPart(decompress(decode(event.getAwsLogs().getData().getBytes(UTF_8))));
59+
} else if (obj instanceof CloudFormationCustomResourceEvent) {
60+
CloudFormationCustomResourceEvent event = (CloudFormationCustomResourceEvent) obj;
61+
return new EventPart(event.getResourceProperties());
62+
} else if (obj instanceof KinesisEvent) {
63+
KinesisEvent event = (KinesisEvent) obj;
64+
return new EventPart(event.getRecords().stream().map(r -> decode(r.getKinesis().getData())).collect(Collectors.toList()));
65+
} else if (obj instanceof KinesisFirehoseEvent) {
66+
KinesisFirehoseEvent event = (KinesisFirehoseEvent) obj;
67+
return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList()));
68+
} else if (obj instanceof KafkaEvent) {
69+
KafkaEvent event = (KafkaEvent) obj;
70+
return new EventPart(event.getRecords().values().stream().flatMap(List::stream).map(r -> decode(r.getValue())).collect(Collectors.toList()));
71+
} else if (obj instanceof ActiveMQEvent) {
72+
ActiveMQEvent event = (ActiveMQEvent) obj;
73+
return new EventPart(event.getMessages().stream().map(m -> decode(m.getData())).collect(Collectors.toList()));
74+
} else if (obj instanceof RabbitMQEvent) {
75+
RabbitMQEvent event = (RabbitMQEvent) obj;
76+
return new EventPart(event.getRmqMessagesByQueue().values().stream().flatMap(List::stream).map(r -> decode(r.getData())).collect(Collectors.toList()));
77+
} else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) {
78+
KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj;
79+
return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList()));
80+
} else if (obj instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) {
81+
KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) obj;
82+
return new EventPart(event.getRecords().stream().map(r -> decode(r.getData())).collect(Collectors.toList()));
83+
} else {
84+
// does not really make sense to use this EventLoader when you already have a typed object
85+
// just not to throw an exception
86+
LOG.warn("Consider using your object directly instead of using EventDeserializer");
87+
return new EventPart(obj);
88+
}
89+
}
90+
91+
public static class EventPart {
92+
private Map<String, Object> contentMap;
93+
private String content;
94+
private List<String> contentList;
95+
private Object contentObject;
96+
97+
public EventPart(List<String> contentList) {
98+
this.contentList = contentList;
99+
}
100+
101+
public EventPart(String content) {
102+
this.content = content;
103+
}
104+
105+
public EventPart(Map<String, Object> contentMap) {
106+
this.contentMap = contentMap;
107+
}
108+
109+
public EventPart(Object content) {
110+
this.contentObject = content;
111+
}
112+
113+
public <T> T extractDataAs(Class<T> clazz) {
114+
try {
115+
if (content != null) {
116+
if (content.getClass().equals(clazz)) {
117+
// do not read json when returning String, just return the String
118+
return (T) content;
119+
}
120+
return JsonConfig.get().getObjectMapper().reader().readValue(content, clazz);
121+
}
122+
if (contentMap != null) {
123+
return JsonConfig.get().getObjectMapper().convertValue(contentMap, clazz);
124+
}
125+
if (contentObject != null) {
126+
return (T) contentObject;
127+
}
128+
if (contentList != null) {
129+
throw new EventDeserializationException("The content of this event is a list, consider using 'extractDataAsListOf' instead");
130+
}
131+
throw new EventDeserializationException("Event content is null");
132+
} catch (IOException e) {
133+
throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e);
134+
}
135+
}
136+
137+
public <T> List<T> extractDataAsListOf(Class<T> clazz) {
138+
if (contentList == null) {
139+
throw new EventDeserializationException("Event content is null");
140+
}
141+
return contentList.stream().map(s -> {
142+
try {
143+
return JsonConfig.get().getObjectMapper().reader().readValue(s, clazz);
144+
} catch (IOException e) {
145+
throw new EventDeserializationException("Cannot load the event as " + clazz.getSimpleName(), e);
146+
}
147+
}).collect(Collectors.toList());
148+
}
149+
}
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
package software.amazon.lambda.powertools.utilities;
15+
16+
import com.amazonaws.services.lambda.runtime.events.*;
17+
import com.amazonaws.services.lambda.runtime.tests.annotations.Event;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import software.amazon.lambda.powertools.utilities.model.Basket;
21+
import software.amazon.lambda.powertools.utilities.model.Product;
22+
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
29+
import static software.amazon.lambda.powertools.utilities.EventDeserializer.from;
30+
31+
public class EventDeserializerTest {
32+
33+
@Test
34+
public void testDeserializeStringAsString_shouldReturnString() {
35+
String stringEvent = "Hello World";
36+
String result = from(stringEvent).extractDataAs(String.class);
37+
assertThat(result).isEqualTo(stringEvent);
38+
}
39+
40+
@Test
41+
public void testDeserializeStringAsObject_shouldReturnObject() {
42+
String productStr = "{\"id\":1234, \"name\":\"product\", \"price\":42}";
43+
Product product = from(productStr).extractDataAs(Product.class);
44+
assertProduct(product);
45+
}
46+
47+
@Test
48+
public void testDeserializeMapAsObject_shouldReturnObject() {
49+
Map<String, Object> map = new HashMap<>();
50+
map.put("id", 1234);
51+
map.put("name", "product");
52+
map.put("price", 42);
53+
Product product = from(map).extractDataAs(Product.class);
54+
assertProduct(product);
55+
}
56+
57+
@ParameterizedTest
58+
@Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class)
59+
public void testDeserializeAPIGWEventBodyAsObject_shouldReturnObject(APIGatewayProxyRequestEvent event) {
60+
Product product = from(event).extractDataAs(Product.class);
61+
assertProduct(product);
62+
}
63+
64+
@ParameterizedTest
65+
@Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class)
66+
public void testDeserializeAPIGWEventBodyAsWrongObjectType_shouldThrowException(APIGatewayProxyRequestEvent event) {
67+
assertThatThrownBy(() -> from(event).extractDataAs(Basket.class))
68+
.isInstanceOf(EventDeserializationException.class)
69+
.hasMessage("Cannot load the event as Basket");
70+
}
71+
72+
@ParameterizedTest
73+
@Event(value = "sns_event.json", type = SNSEvent.class)
74+
public void testDeserializeSNSEventMessageAsObject_shouldReturnObject(SNSEvent event) {
75+
Product product = from(event).extractDataAs(Product.class);
76+
assertProduct(product);
77+
}
78+
79+
@ParameterizedTest
80+
@Event(value = "sqs_event.json", type = SQSEvent.class)
81+
public void testDeserializeSQSEventMessageAsList_shouldReturnList(SQSEvent event) {
82+
List<Product> products = from(event).extractDataAsListOf(Product.class);
83+
assertThat(products).hasSize(2);
84+
assertProduct(products.get(0));
85+
}
86+
87+
@ParameterizedTest
88+
@Event(value = "kinesis_event.json", type = KinesisEvent.class)
89+
public void testDeserializeKinesisEventMessageAsList_shouldReturnList(KinesisEvent event) {
90+
List<Product> products = from(event).extractDataAsListOf(Product.class);
91+
assertThat(products).hasSize(2);
92+
assertProduct(products.get(0));
93+
}
94+
95+
@ParameterizedTest
96+
@Event(value = "kafka_event.json", type = KafkaEvent.class)
97+
public void testDeserializeKafkaEventMessageAsList_shouldReturnList(KafkaEvent event) {
98+
List<Product> products = from(event).extractDataAsListOf(Product.class);
99+
assertThat(products).hasSize(2);
100+
assertProduct(products.get(0));
101+
}
102+
103+
@ParameterizedTest
104+
@Event(value = "sqs_event.json", type = SQSEvent.class)
105+
public void testDeserializeSQSEventMessageAsObject_shouldThrowException(SQSEvent event) {
106+
assertThatThrownBy(() -> from(event).extractDataAs(Product.class))
107+
.isInstanceOf(EventDeserializationException.class)
108+
.hasMessageContaining("consider using 'extractDataAsListOf' instead");
109+
}
110+
111+
@Test
112+
public void testDeserializeProductAsProduct_shouldReturnProduct() {
113+
Product myProduct = new Product(1234, "product", 42);
114+
Product product = from(myProduct).extractDataAs(Product.class);
115+
assertProduct(product);
116+
}
117+
118+
119+
private void assertProduct(Product product) {
120+
assertThat(product.getId()).isEqualTo(1234);
121+
assertThat(product.getName()).isEqualTo("product");
122+
assertThat(product.getPrice()).isEqualTo(42);
123+
}
124+
125+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
package software.amazon.lambda.powertools.utilities.model;
15+
16+
import java.util.ArrayList;
17+
import java.util.Arrays;
18+
import java.util.List;
19+
import java.util.Objects;
20+
21+
public class Basket {
22+
private List<Product> products = new ArrayList<>();
23+
24+
public List<Product> getProducts() {
25+
return products;
26+
}
27+
28+
public void setProducts(List<Product> products) {
29+
this.products = products;
30+
}
31+
32+
public Basket() {
33+
}
34+
35+
public Basket( Product ...p){
36+
products.addAll(Arrays.asList(p));
37+
}
38+
39+
public void add(Product product) {
40+
products.add(product);
41+
}
42+
43+
@Override
44+
public boolean equals(Object o) {
45+
if (this == o) return true;
46+
if (o == null || getClass() != o.getClass()) return false;
47+
Basket basket = (Basket) o;
48+
return products.equals(basket.products);
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return Objects.hash(products);
54+
}
55+
}

0 commit comments

Comments
 (0)