Skip to content

Commit 5d2b640

Browse files
authored
Add duplicateAndRenameSharedEvents customization to support event shapes shared with multiple evenstreams (#6031)
* Add processor to detect shared events + customization to duplicate. * Fix null memberShape * Improve test coverage * Add changelog * Deep copy the new event shape * Convert exceptions to ModelValidations
1 parent 303d19b commit 5d2b640

File tree

20 files changed

+469
-17
lines changed

20 files changed

+469
-17
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Add duplicateAndRenameSharedEvents customization to support event shapes shared with multiple evenstreams."
6+
}

codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/DefaultCustomizationProcessor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public static CodegenCustomizationProcessor getProcessorFor(
3838
new RemoveExceptionMessagePropertyProcessor(),
3939
new UseLegacyEventGenerationSchemeProcessor(),
4040
new NewAndLegacyEventStreamProcessor(),
41+
new EventStreamSharedEventProcessor(config.getDuplicateAndRenameSharedEvents()),
4142
new S3RemoveBucketFromUriProcessor(),
4243
new S3ControlRemoveAccountIdHostPrefixProcessor(),
4344
new ExplicitStringPayloadQueryProtocolProcessor(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.codegen.customization.processors;
17+
18+
import java.io.IOException;
19+
import java.io.StringWriter;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import software.amazon.awssdk.codegen.customization.CodegenCustomizationProcessor;
25+
import software.amazon.awssdk.codegen.internal.Jackson;
26+
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
27+
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
28+
import software.amazon.awssdk.codegen.model.service.Member;
29+
import software.amazon.awssdk.codegen.model.service.ServiceModel;
30+
import software.amazon.awssdk.codegen.model.service.Shape;
31+
import software.amazon.awssdk.codegen.validation.ModelInvalidException;
32+
import software.amazon.awssdk.codegen.validation.ValidationEntry;
33+
import software.amazon.awssdk.codegen.validation.ValidationErrorId;
34+
import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity;
35+
36+
/**
37+
* Processor for eventstreams with shared events. This Processor does two things: 1. Apply the duplicateAndRenameSharedEvents
38+
* customization 2. Raise helpful error messages on untransfromed shared events.
39+
*/
40+
public final class EventStreamSharedEventProcessor implements CodegenCustomizationProcessor {
41+
private static final Logger log = LoggerFactory.getLogger(EventStreamSharedEventProcessor.class);
42+
43+
private final Map<String, Map<String, String>> duplicateAndRenameSharedEvents;
44+
45+
public EventStreamSharedEventProcessor(Map<String, Map<String, String>> duplicateAndRenameSharedEvents) {
46+
this.duplicateAndRenameSharedEvents = duplicateAndRenameSharedEvents;
47+
}
48+
49+
@Override
50+
public void preprocess(ServiceModel serviceModel) {
51+
if (duplicateAndRenameSharedEvents == null || duplicateAndRenameSharedEvents.isEmpty()) {
52+
return;
53+
}
54+
55+
for (Map.Entry<String, Map<String, String>> eventStreamEntry : duplicateAndRenameSharedEvents.entrySet()) {
56+
57+
String eventStreamName = eventStreamEntry.getKey();
58+
Shape eventStreamShape = serviceModel.getShapes().get(eventStreamName);
59+
60+
validateIsEventStream(eventStreamShape, eventStreamName);
61+
62+
Map<String, Member> eventStreamMembers = eventStreamShape.getMembers();
63+
for (Map.Entry<String, String> eventEntry : eventStreamEntry.getValue().entrySet()) {
64+
Member eventMemberToModify = eventStreamMembers.get(eventEntry.getKey());
65+
66+
if (eventMemberToModify == null) {
67+
throw ModelInvalidException.fromEntry(ValidationEntry.create(
68+
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
69+
ValidationErrorSeverity.DANGER,
70+
String.format("Cannot find event member [%s] in the eventstream [%s] when processing "
71+
+ "customization config duplicateAndRenameSharedEvents.%s",
72+
eventEntry.getKey(), eventStreamName, eventStreamName)));
73+
}
74+
75+
String shapeToDuplicate = eventMemberToModify.getShape();
76+
Shape eventMemberShape = serviceModel.getShape(shapeToDuplicate);
77+
78+
if (eventMemberShape == null || !eventMemberShape.isEvent()) {
79+
throw ModelInvalidException.fromEntry(ValidationEntry.create(
80+
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
81+
ValidationErrorSeverity.DANGER,
82+
String.format("Error: [%s] must be an Event shape when processing "
83+
+ "customization config duplicateAndRenameSharedEvents.%s",
84+
eventEntry.getKey(), eventStreamName)));
85+
}
86+
87+
String newShapeName = eventEntry.getValue();
88+
if (serviceModel.getShapes().containsKey(newShapeName)) {
89+
throw ModelInvalidException.fromEntry(ValidationEntry.create(
90+
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
91+
ValidationErrorSeverity.DANGER,
92+
String.format("Error: [%s] is already in the model when processing "
93+
+ "customization config duplicateAndRenameSharedEvents.%s",
94+
newShapeName, eventStreamName)));
95+
}
96+
serviceModel.getShapes().put(newShapeName, duplicateShape(eventMemberShape));
97+
eventMemberToModify.setShape(newShapeName);
98+
log.info("Duplicated and renamed event member on {} from {} -> {}",
99+
eventStreamName, shapeToDuplicate, newShapeName);
100+
}
101+
}
102+
}
103+
104+
private Shape duplicateShape(Shape shape) {
105+
StringWriter writer = new StringWriter();
106+
try {
107+
Jackson.writeWithObjectMapper(shape, writer);
108+
Shape duplicated = Jackson.load(Shape.class, writer.toString());
109+
duplicated.setSynthetic(true);
110+
return duplicated;
111+
} catch (IOException e) {
112+
throw new RuntimeException(e);
113+
}
114+
}
115+
116+
private static void validateIsEventStream(Shape shape, String name) {
117+
if (shape == null) {
118+
throw ModelInvalidException.fromEntry(ValidationEntry.create(
119+
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
120+
ValidationErrorSeverity.DANGER,
121+
String.format("Cannot find eventstream shape [%s] in the model when processing "
122+
+ "customization config duplicateAndRenameSharedEvents.%s", name, name)));
123+
}
124+
if (!shape.isEventstream()) {
125+
throw ModelInvalidException.fromEntry(ValidationEntry.create(
126+
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
127+
ValidationErrorSeverity.DANGER,
128+
String.format("Error: [%s] must be an EventStream when processing "
129+
+ "customization config duplicateAndRenameSharedEvents.%s", name, name)));
130+
}
131+
}
132+
133+
@Override
134+
public void postprocess(IntermediateModel intermediateModel) {
135+
// validate that there are no events shared between multiple eventstreams.
136+
// events may be used multiple times in the same eventstream.
137+
Map<String, String> seenEvents = new HashMap<>();
138+
139+
for (ShapeModel shapeModel : intermediateModel.getShapes().values()) {
140+
if (shapeModel.isEventStream()) {
141+
shapeModel.getMembers().forEach(m -> {
142+
ShapeModel memberShape = intermediateModel.getShapes().get(m.getC2jShape());
143+
if (memberShape != null && memberShape.isEvent()) {
144+
if (seenEvents.containsKey(memberShape.getShapeName())
145+
&& !seenEvents.get(memberShape.getShapeName()).equals(shapeModel.getShapeName())) {
146+
throw ModelInvalidException.fromEntry(ValidationEntry.create(
147+
ValidationErrorId.SHARED_EVENTSTREAM_EVENT,
148+
ValidationErrorSeverity.DANGER,
149+
String.format("Event shape `%s` is shared between multiple EventStreams. Apply the "
150+
+ "duplicateAndRenameSharedEvents customization to resolve the issue.",
151+
memberShape.getShapeName())
152+
));
153+
}
154+
seenEvents.put(memberShape.getShapeName(), shapeModel.getShapeName());
155+
}
156+
});
157+
}
158+
}
159+
}
160+
}

codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ public class CustomizationConfig {
210210
*/
211211
private Map<String, List<String>> useLegacyEventGenerationScheme = new HashMap<>();
212212

213+
/**
214+
* Customization to instruct the code generator to duplicate and rename an event that is shared
215+
* by multiple EventStreams.
216+
*/
217+
private Map<String, Map<String, String>> duplicateAndRenameSharedEvents = new HashMap<>();
218+
213219
/**
214220
* How the code generator should behave when it encounters shapes with underscores in the name.
215221
*/
@@ -666,6 +672,14 @@ public void setUseLegacyEventGenerationScheme(Map<String, List<String>> useLegac
666672
this.useLegacyEventGenerationScheme = useLegacyEventGenerationScheme;
667673
}
668674

675+
public Map<String, Map<String, String>> getDuplicateAndRenameSharedEvents() {
676+
return duplicateAndRenameSharedEvents;
677+
}
678+
679+
public void setDuplicateAndRenameSharedEvents(Map<String, Map<String, String>> duplicateAndRenameSharedEvents) {
680+
this.duplicateAndRenameSharedEvents = duplicateAndRenameSharedEvents;
681+
}
682+
669683
public UnderscoresInNameBehavior getUnderscoresInNameBehavior() {
670684
return underscoresInNameBehavior;
671685
}

codegen/src/main/java/software/amazon/awssdk/codegen/model/service/Shape.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.awssdk.codegen.model.service;
1717

18+
import com.fasterxml.jackson.annotation.JsonIgnore;
1819
import java.util.Collections;
1920
import java.util.List;
2021
import java.util.Map;
@@ -352,10 +353,12 @@ public void setRetryable(RetryableTrait retryable) {
352353
this.retryable = retryable;
353354
}
354355

356+
@JsonIgnore
355357
public boolean isRetryable() {
356358
return retryable != null;
357359
}
358360

361+
@JsonIgnore
359362
public boolean isThrottling() {
360363
return retryable != null && retryable.isThrottling();
361364
}

codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorId.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ public enum ValidationErrorId {
2626
INVALID_CODEGEN_CUSTOMIZATION("A customization is enabled for this service that cannot be applied for the given service "
2727
+ "model."),
2828
UNKNOWN_OPERATION("The model references an unknown operation."),
29-
INVALID_IDENTIFIER_NAME("The model contains an invalid or non-idiomatic name or identifier.");
29+
INVALID_IDENTIFIER_NAME("The model contains an invalid or non-idiomatic name or identifier."),
30+
SHARED_EVENTSTREAM_EVENT("Event shape shared between multiple EventStreams. "
31+
+ "Missing duplicateAndRenameSharedEvents customization.");
3032

3133
private final String description;
3234

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.codegen.customization.processors;
17+
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
import static org.junit.Assert.assertNotEquals;
20+
import static org.junit.Assert.assertTrue;
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertNotNull;
23+
24+
import java.io.File;
25+
import java.util.Map;
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
import software.amazon.awssdk.codegen.C2jModels;
29+
import software.amazon.awssdk.codegen.IntermediateModelBuilder;
30+
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
31+
import software.amazon.awssdk.codegen.model.service.ServiceModel;
32+
import software.amazon.awssdk.codegen.model.service.Shape;
33+
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils;
34+
import software.amazon.awssdk.codegen.validation.ModelInvalidException;
35+
import software.amazon.awssdk.utils.ImmutableMap;
36+
37+
public class EventStreamSharedEventProcessorTest {
38+
private static final String RESOURCE_ROOT = "/software/amazon/awssdk/codegen/customization/processors"
39+
+ "/eventstreamsharedeventprocessor/";
40+
41+
private ServiceModel serviceModel;
42+
43+
@Before
44+
public void setUp() {
45+
File serviceModelFile =
46+
new File(EventStreamSharedEventProcessorTest.class.getResource(RESOURCE_ROOT + "service-2.json").getFile());
47+
serviceModel = ModelLoaderUtils.loadModel(ServiceModel.class, serviceModelFile);
48+
}
49+
50+
@Test
51+
public void duplicatesAndRenamesSharedEvent() {
52+
File customizationConfigFile =
53+
new File(EventStreamSharedEventProcessorTest.class.getResource(RESOURCE_ROOT + "customization.config").getFile());
54+
CustomizationConfig config = ModelLoaderUtils.loadModel(CustomizationConfig.class, customizationConfigFile);
55+
56+
EventStreamSharedEventProcessor processor =
57+
new EventStreamSharedEventProcessor(config.getDuplicateAndRenameSharedEvents());
58+
processor.preprocess(serviceModel);
59+
60+
Shape newEventShape = serviceModel.getShape("PayloadB");
61+
assertNotNull(newEventShape);
62+
assertNotEquals(serviceModel.getShape("Payload"), newEventShape); // shape is duplicated/deep copied
63+
assertTrue(newEventShape.isSynthetic());
64+
assertEquals(serviceModel.getShape("Payload").getType(), newEventShape.getType());
65+
assertEquals(serviceModel.getShape("Payload").getMembers().keySet(), newEventShape.getMembers().keySet());
66+
67+
Shape streamB = serviceModel.getShape("StreamB");
68+
assertEquals("PayloadB", streamB.getMembers().get("Payload").getShape());
69+
}
70+
71+
@Test
72+
public void modelWithSharedEvents_raises() {
73+
CustomizationConfig emptyConfig = CustomizationConfig.create();
74+
75+
assertThatThrownBy(() -> new IntermediateModelBuilder(
76+
C2jModels.builder()
77+
.serviceModel(serviceModel)
78+
.customizationConfig(emptyConfig)
79+
.build()).build())
80+
.isInstanceOf(ModelInvalidException.class)
81+
.hasMessageContaining("Event shape `Payload` is shared between multiple EventStreams");
82+
}
83+
84+
@Test
85+
public void invalidCustomization_missingShape() {
86+
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of("MissingShape", null);
87+
88+
EventStreamSharedEventProcessor processor =
89+
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
90+
assertThatThrownBy(() -> processor.preprocess(serviceModel))
91+
.isInstanceOf(ModelInvalidException.class)
92+
.hasMessageContaining("Cannot find eventstream shape [MissingShape]");
93+
}
94+
95+
@Test
96+
public void invalidCustomization_notEventStream() {
97+
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of("Payload", null);
98+
99+
EventStreamSharedEventProcessor processor =
100+
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
101+
assertThatThrownBy(() -> processor.preprocess(serviceModel))
102+
.isInstanceOf(ModelInvalidException.class)
103+
.hasMessageContaining("Error: [Payload] must be an EventStream");
104+
}
105+
106+
@Test
107+
public void invalidCustomization_invalidMember() {
108+
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of(
109+
"StreamB", ImmutableMap.of("InvalidMember", "Payload"));
110+
111+
EventStreamSharedEventProcessor processor =
112+
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
113+
assertThatThrownBy(() -> processor.preprocess(serviceModel))
114+
.isInstanceOf(ModelInvalidException.class)
115+
.hasMessageContaining("Cannot find event member [InvalidMember] in the eventstream [StreamB]");
116+
}
117+
118+
@Test
119+
public void invalidCustomization_shapeAlreadyExists() {
120+
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of(
121+
"StreamB", ImmutableMap.of("Payload", "Payload"));
122+
123+
EventStreamSharedEventProcessor processor =
124+
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
125+
assertThatThrownBy(() -> processor.preprocess(serviceModel))
126+
.isInstanceOf(ModelInvalidException.class)
127+
.hasMessageContaining("Error: [Payload] is already in the model");
128+
}
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"duplicateAndRenameSharedEvents": {
3+
"StreamB": {
4+
"Payload": "PayloadB"
5+
}
6+
}
7+
}

0 commit comments

Comments
 (0)