Skip to content

Add processor to detect shared events + customization to duplicate. #6031

New issue

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

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

Already on GitHub? # to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
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);
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) {
// 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 @@ -654,6 +660,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