Skip to content

Add duplicateAndRenameSharedEvents customization to support event shapes shared with multiple evenstreams #6031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-aee20f7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Add duplicateAndRenameSharedEvents customization to support event shapes shared with multiple evenstreams."
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static CodegenCustomizationProcessor getProcessorFor(
new RemoveExceptionMessagePropertyProcessor(),
new UseLegacyEventGenerationSchemeProcessor(),
new NewAndLegacyEventStreamProcessor(),
new EventStreamSharedEventProcessor(config.getDuplicateAndRenameSharedEvents()),
new S3RemoveBucketFromUriProcessor(),
new S3ControlRemoveAccountIdHostPrefixProcessor(),
new ExplicitStringPayloadQueryProtocolProcessor(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.awssdk.codegen.customization.processors;

import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.codegen.customization.CodegenCustomizationProcessor;
import software.amazon.awssdk.codegen.internal.Jackson;
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
import software.amazon.awssdk.codegen.model.service.Member;
import software.amazon.awssdk.codegen.model.service.ServiceModel;
import software.amazon.awssdk.codegen.model.service.Shape;
import software.amazon.awssdk.codegen.validation.ModelInvalidException;
import software.amazon.awssdk.codegen.validation.ValidationEntry;
import software.amazon.awssdk.codegen.validation.ValidationErrorId;
import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity;

/**
* Processor for eventstreams with shared events. This Processor does two things: 1. Apply the duplicateAndRenameSharedEvents
* customization 2. Raise helpful error messages on untransfromed shared events.
*/
public final class EventStreamSharedEventProcessor implements CodegenCustomizationProcessor {
private static final Logger log = LoggerFactory.getLogger(EventStreamSharedEventProcessor.class);

private final Map<String, Map<String, String>> duplicateAndRenameSharedEvents;

public EventStreamSharedEventProcessor(Map<String, Map<String, String>> duplicateAndRenameSharedEvents) {
this.duplicateAndRenameSharedEvents = duplicateAndRenameSharedEvents;
}

@Override
public void preprocess(ServiceModel serviceModel) {
if (duplicateAndRenameSharedEvents == null || duplicateAndRenameSharedEvents.isEmpty()) {
return;
}

for (Map.Entry<String, Map<String, String>> eventStreamEntry : duplicateAndRenameSharedEvents.entrySet()) {

String eventStreamName = eventStreamEntry.getKey();
Shape eventStreamShape = serviceModel.getShapes().get(eventStreamName);

validateIsEventStream(eventStreamShape, eventStreamName);

Map<String, Member> eventStreamMembers = eventStreamShape.getMembers();
for (Map.Entry<String, String> eventEntry : eventStreamEntry.getValue().entrySet()) {
Member eventMemberToModify = eventStreamMembers.get(eventEntry.getKey());

if (eventMemberToModify == null) {
throw ModelInvalidException.fromEntry(ValidationEntry.create(
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
ValidationErrorSeverity.DANGER,
String.format("Cannot find event member [%s] in the eventstream [%s] when processing "
+ "customization config duplicateAndRenameSharedEvents.%s",
eventEntry.getKey(), eventStreamName, eventStreamName)));
}

String shapeToDuplicate = eventMemberToModify.getShape();
Shape eventMemberShape = serviceModel.getShape(shapeToDuplicate);

if (eventMemberShape == null || !eventMemberShape.isEvent()) {
throw ModelInvalidException.fromEntry(ValidationEntry.create(
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
ValidationErrorSeverity.DANGER,
String.format("Error: [%s] must be an Event shape when processing "
+ "customization config duplicateAndRenameSharedEvents.%s",
eventEntry.getKey(), eventStreamName)));
}

String newShapeName = eventEntry.getValue();
if (serviceModel.getShapes().containsKey(newShapeName)) {
throw ModelInvalidException.fromEntry(ValidationEntry.create(
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
ValidationErrorSeverity.DANGER,
String.format("Error: [%s] is already in the model when processing "
+ "customization config duplicateAndRenameSharedEvents.%s",
newShapeName, eventStreamName)));
}
serviceModel.getShapes().put(newShapeName, duplicateShape(eventMemberShape));
eventMemberToModify.setShape(newShapeName);
log.info("Duplicated and renamed event member on {} from {} -> {}",
eventStreamName, shapeToDuplicate, newShapeName);
}
}
}

private Shape duplicateShape(Shape shape) {
StringWriter writer = new StringWriter();
try {
Jackson.writeWithObjectMapper(shape, writer);
Shape duplicated = Jackson.load(Shape.class, writer.toString());
duplicated.setSynthetic(true);
return duplicated;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static void validateIsEventStream(Shape shape, String name) {
if (shape == null) {
throw ModelInvalidException.fromEntry(ValidationEntry.create(
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
ValidationErrorSeverity.DANGER,
String.format("Cannot find eventstream shape [%s] in the model when processing "
+ "customization config duplicateAndRenameSharedEvents.%s", name, name)));
}
if (!shape.isEventstream()) {
throw ModelInvalidException.fromEntry(ValidationEntry.create(
ValidationErrorId.INVALID_CODEGEN_CUSTOMIZATION,
ValidationErrorSeverity.DANGER,
String.format("Error: [%s] must be an EventStream when processing "
+ "customization config duplicateAndRenameSharedEvents.%s", name, name)));
}
}

@Override
public void postprocess(IntermediateModel intermediateModel) {
// validate that there are no events shared between multiple eventstreams.
// events may be used multiple times in the same eventstream.
Map<String, String> seenEvents = new HashMap<>();

for (ShapeModel shapeModel : intermediateModel.getShapes().values()) {
if (shapeModel.isEventStream()) {
shapeModel.getMembers().forEach(m -> {
ShapeModel memberShape = intermediateModel.getShapes().get(m.getC2jShape());
if (memberShape != null && memberShape.isEvent()) {
if (seenEvents.containsKey(memberShape.getShapeName())
&& !seenEvents.get(memberShape.getShapeName()).equals(shapeModel.getShapeName())) {
throw ModelInvalidException.fromEntry(ValidationEntry.create(
ValidationErrorId.SHARED_EVENTSTREAM_EVENT,
ValidationErrorSeverity.DANGER,
String.format("Event shape `%s` is shared between multiple EventStreams. Apply the "
+ "duplicateAndRenameSharedEvents customization to resolve the issue.",
memberShape.getShapeName())
));
}
seenEvents.put(memberShape.getShapeName(), shapeModel.getShapeName());
}
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ public class CustomizationConfig {
*/
private Map<String, List<String>> useLegacyEventGenerationScheme = new HashMap<>();

/**
* Customization to instruct the code generator to duplicate and rename an event that is shared
* by multiple EventStreams.
*/
private Map<String, Map<String, String>> duplicateAndRenameSharedEvents = new HashMap<>();

/**
* How the code generator should behave when it encounters shapes with underscores in the name.
*/
Expand Down Expand Up @@ -666,6 +672,14 @@ public void setUseLegacyEventGenerationScheme(Map<String, List<String>> useLegac
this.useLegacyEventGenerationScheme = useLegacyEventGenerationScheme;
}

public Map<String, Map<String, String>> getDuplicateAndRenameSharedEvents() {
return duplicateAndRenameSharedEvents;
}

public void setDuplicateAndRenameSharedEvents(Map<String, Map<String, String>> duplicateAndRenameSharedEvents) {
this.duplicateAndRenameSharedEvents = duplicateAndRenameSharedEvents;
}

public UnderscoresInNameBehavior getUnderscoresInNameBehavior() {
return underscoresInNameBehavior;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

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

import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -352,10 +353,12 @@ public void setRetryable(RetryableTrait retryable) {
this.retryable = retryable;
}

@JsonIgnore
public boolean isRetryable() {
return retryable != null;
}

@JsonIgnore
public boolean isThrottling() {
return retryable != null && retryable.isThrottling();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ public enum ValidationErrorId {
INVALID_CODEGEN_CUSTOMIZATION("A customization is enabled for this service that cannot be applied for the given service "
+ "model."),
UNKNOWN_OPERATION("The model references an unknown operation."),
INVALID_IDENTIFIER_NAME("The model contains an invalid or non-idiomatic name or identifier.");
INVALID_IDENTIFIER_NAME("The model contains an invalid or non-idiomatic name or identifier."),
SHARED_EVENTSTREAM_EVENT("Event shape shared between multiple EventStreams. "
+ "Missing duplicateAndRenameSharedEvents customization.");

private final String description;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.awssdk.codegen.customization.processors;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.io.File;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import software.amazon.awssdk.codegen.C2jModels;
import software.amazon.awssdk.codegen.IntermediateModelBuilder;
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
import software.amazon.awssdk.codegen.model.service.ServiceModel;
import software.amazon.awssdk.codegen.model.service.Shape;
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils;
import software.amazon.awssdk.codegen.validation.ModelInvalidException;
import software.amazon.awssdk.utils.ImmutableMap;

public class EventStreamSharedEventProcessorTest {
private static final String RESOURCE_ROOT = "/software/amazon/awssdk/codegen/customization/processors"
+ "/eventstreamsharedeventprocessor/";

private ServiceModel serviceModel;

@Before
public void setUp() {
File serviceModelFile =
new File(EventStreamSharedEventProcessorTest.class.getResource(RESOURCE_ROOT + "service-2.json").getFile());
serviceModel = ModelLoaderUtils.loadModel(ServiceModel.class, serviceModelFile);
}

@Test
public void duplicatesAndRenamesSharedEvent() {
File customizationConfigFile =
new File(EventStreamSharedEventProcessorTest.class.getResource(RESOURCE_ROOT + "customization.config").getFile());
CustomizationConfig config = ModelLoaderUtils.loadModel(CustomizationConfig.class, customizationConfigFile);

EventStreamSharedEventProcessor processor =
new EventStreamSharedEventProcessor(config.getDuplicateAndRenameSharedEvents());
processor.preprocess(serviceModel);

Shape newEventShape = serviceModel.getShape("PayloadB");
assertNotNull(newEventShape);
assertNotEquals(serviceModel.getShape("Payload"), newEventShape); // shape is duplicated/deep copied
assertTrue(newEventShape.isSynthetic());
assertEquals(serviceModel.getShape("Payload").getType(), newEventShape.getType());
assertEquals(serviceModel.getShape("Payload").getMembers().keySet(), newEventShape.getMembers().keySet());

Shape streamB = serviceModel.getShape("StreamB");
assertEquals("PayloadB", streamB.getMembers().get("Payload").getShape());
}

@Test
public void modelWithSharedEvents_raises() {
CustomizationConfig emptyConfig = CustomizationConfig.create();

assertThatThrownBy(() -> new IntermediateModelBuilder(
C2jModels.builder()
.serviceModel(serviceModel)
.customizationConfig(emptyConfig)
.build()).build())
.isInstanceOf(ModelInvalidException.class)
.hasMessageContaining("Event shape `Payload` is shared between multiple EventStreams");
}

@Test
public void invalidCustomization_missingShape() {
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of("MissingShape", null);

EventStreamSharedEventProcessor processor =
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
assertThatThrownBy(() -> processor.preprocess(serviceModel))
.isInstanceOf(ModelInvalidException.class)
.hasMessageContaining("Cannot find eventstream shape [MissingShape]");
}

@Test
public void invalidCustomization_notEventStream() {
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of("Payload", null);

EventStreamSharedEventProcessor processor =
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
assertThatThrownBy(() -> processor.preprocess(serviceModel))
.isInstanceOf(ModelInvalidException.class)
.hasMessageContaining("Error: [Payload] must be an EventStream");
}

@Test
public void invalidCustomization_invalidMember() {
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of(
"StreamB", ImmutableMap.of("InvalidMember", "Payload"));

EventStreamSharedEventProcessor processor =
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
assertThatThrownBy(() -> processor.preprocess(serviceModel))
.isInstanceOf(ModelInvalidException.class)
.hasMessageContaining("Cannot find event member [InvalidMember] in the eventstream [StreamB]");
}

@Test
public void invalidCustomization_shapeAlreadyExists() {
Map<String, Map<String, String>> duplicateAndRenameSharedEvents = ImmutableMap.of(
"StreamB", ImmutableMap.of("Payload", "Payload"));

EventStreamSharedEventProcessor processor =
new EventStreamSharedEventProcessor(duplicateAndRenameSharedEvents);
assertThatThrownBy(() -> processor.preprocess(serviceModel))
.isInstanceOf(ModelInvalidException.class)
.hasMessageContaining("Error: [Payload] is already in the model");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"duplicateAndRenameSharedEvents": {
"StreamB": {
"Payload": "PayloadB"
}
}
}
Loading
Loading