Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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,128 @@
/*
* 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.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.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;

/**
* 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 new IllegalStateException(
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 new IllegalStateException(
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 new IllegalStateException(
String.format("Error: [%s] is already in the model when processing "
+ "customization config duplicateAndRenameSharedEvents.%s",
newShapeName, eventStreamName));
}
serviceModel.getShapes().put(newShapeName, eventMemberShape);
Comment thread
Fred1155 marked this conversation as resolved.
Outdated
eventMemberToModify.setShape(newShapeName);
log.info("Duplicated and renamed event member on {} from {} -> {}",
eventStreamName, shapeToDuplicate, newShapeName);
}
}
}

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

@Override
public void postprocess(IntermediateModel intermediateModel) {
Comment thread
RanVaknin marked this conversation as resolved.
// 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 new IllegalStateException(
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
@@ -0,0 +1,123 @@
/*
* 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.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.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);
assertEquals(serviceModel.getShape("Payload"), newEventShape);

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(IllegalStateException.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(IllegalStateException.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(IllegalStateException.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(IllegalStateException.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(IllegalStateException.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"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"version": "2.0",
"metadata": {
"apiVersion": "2010-05-08",
"endpointPrefix": "shared-event-stream-service",
"globalEndpoint": "shared-event-stream.amazonaws.com",
"protocol": "rest-json",
"serviceAbbreviation": "Shared Event Stream Service",
"serviceFullName": "Service that shares event streams",
"serviceId":"Shared Event Stream Service",
"signatureVersion": "v4",
"uid": "shared-event-stream-service-2010-05-08",
"xmlNamespace": "https://shared-event-stream-service.amazonaws.com/doc/2010-05-08/"
},
"operations": {
"StreamAOperation" : {
"name": "StreamAOperation",
"http": {
"method": "GET",
"requestUri": "/stream-a"
},
"output": {
"shape": "StreamAOutput"
}
},
"StreamBOperation" : {
"name": "StreamBOperation",
"http": {
"method": "GET",
"requestUri": "/stream-b"
},
"output": {
"shape": "StreamBOutput"
}
}
},
"shapes": {
"String": {
"type": "string"
},
"StreamAOutput": {
"type": "structure",
"members": {
"EventStream": {
"shape": "StreamA"
}
}
},
"StreamA": {
"type": "structure",
"members": {
"Payload": {
"shape": "Payload"
}
},
"eventstream": true
},
"StreamBOutput": {
"type": "structure",
"members": {
"EventStream": {
"shape": "StreamB"
}
}
},
"StreamB": {
"type": "structure",
"members": {
"Payload": {
"shape": "Payload"
}
},
"eventstream": true
},
"Payload": {
"type": "structure",
"members": {
"chunk": {
"shape": "String"
}
},
"event": true
}
},
"documentation": "A service that streams births and deaths"
}
Loading
Loading