Skip to content

Duplicate all eventstream event shapes + add new legacy event modes #6052

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

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
33f0010
Duplicate all eventstream event shapes + add new legacy event modes
alextwoods Apr 22, 2025
00608dd
Add all existing eventstreams to customization w/ legacy mode
alextwoods Apr 23, 2025
8224ea1
Merge branch 'master' into alexwoo/eventstream-shared-shapes-duplicat…
alextwoods Apr 23, 2025
4b748f4
Fix codegen-generated-classes-test (add legacy customization)
alextwoods Apr 23, 2025
1237efa
Add legacy customization flags to protocol tests
alextwoods Apr 23, 2025
5b35d92
Refactor/cleanup processor + add check for duplicated shape names
alextwoods Apr 24, 2025
9e7e110
Move naming to NamingStrategy
alextwoods Apr 30, 2025
21afe11
Merge branch 'master' into alexwoo/eventstream-shared-shapes-duplicat…
alextwoods May 19, 2025
87b40d8
Introduce new customization config instead of changing old config to …
alextwoods May 19, 2025
1c9ee94
Update other service and test customization configs
alextwoods May 19, 2025
e7f09cf
Merge branch 'master' into alexwoo/eventstream-shared-shapes-duplicat…
alextwoods May 20, 2025
6da34e2
Add changelog
alextwoods May 20, 2025
f6a8603
Fix protocol tests config
alextwoods May 20, 2025
a2d0ce2
Cleanup name
alextwoods May 20, 2025
3baee8d
Update naming strategy + improve codegen testing
alextwoods May 22, 2025
ac3d591
Remove unused test files + fix test
alextwoods May 22, 2025
84ccbbf
Remove unused import
alextwoods May 22, 2025
ce71369
Fix unused exception
alextwoods May 22, 2025
dec0147
Merge branch 'master' into alexwoo/eventstream-shared-shapes-duplicat…
alextwoods Aug 14, 2025
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/feature-AWSSDKforJavav2-c73b965.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Fixes codegeneration issues for eventstreams with shared event shapes: Duplicate and rename all eventstream event shapes with a unique name + add new disableUniqueEventStreamShapePreprocessing customization config."
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private List<IntermediateModelShapeProcessor> createShapeProcessors() {

public IntermediateModel build() {
CodegenCustomizationProcessor customization = DefaultCustomizationProcessor
.getProcessorFor(customConfig);
.getProcessorFor(customConfig, namingStrategy);

customization.preprocess(service);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
import software.amazon.awssdk.codegen.customization.CodegenCustomizationProcessor;
import software.amazon.awssdk.codegen.customization.CodegenCustomizationProcessorChain;
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
import software.amazon.awssdk.codegen.naming.NamingStrategy;

public final class DefaultCustomizationProcessor {

private DefaultCustomizationProcessor() {
}

public static CodegenCustomizationProcessor getProcessorFor(
CustomizationConfig config) {
CustomizationConfig config, NamingStrategy namingStrategy) {

return new CodegenCustomizationProcessorChain(
new MetadataModifiersProcessor(config.getCustomServiceMetadata()),
Expand All @@ -37,6 +38,10 @@ public static CodegenCustomizationProcessor getProcessorFor(
new SmithyRpcV2CborProtocolProcessor(),
new RemoveExceptionMessagePropertyProcessor(),
new UseLegacyEventGenerationSchemeProcessor(),
new EventStreamUniqueEventShapesProcessor(
config.getUseLegacyEventGenerationScheme(),
config.getDisableUniqueEventStreamShapePreprocessing(),
namingStrategy),
new NewAndLegacyEventStreamProcessor(),
new S3RemoveBucketFromUriProcessor(),
new S3ControlRemoveAccountIdHostPrefixProcessor(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.awssdk.codegen.customization.CodegenCustomizationProcessor;
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.service.ServiceModel;
import software.amazon.awssdk.codegen.model.service.Shape;
import software.amazon.awssdk.codegen.naming.NamingStrategy;
import software.amazon.awssdk.utils.Logger;

/**
* Processor for eventstreams that ensures that all eventstream event shapes are unique - for each eventstream/event it creates a
* new shape with a unique name constructed from the EventStream and Event shape names: `[ShapeName][EventStreamName]`. Any legacy
* eventstream/events (configured with the useLegacyEventGenerationScheme customization) are skipped. When an event shape is
* shared between multiple eventstreams, it causes SDK generation/compilation failures. The top level shape POJO implements the
* event stream interface for each stream and the return type of the sdkEventType method conflicts.
*/
public final class EventStreamUniqueEventShapesProcessor implements CodegenCustomizationProcessor {
private static final Logger log = Logger.loggerFor(EventStreamUniqueEventShapesProcessor.class);

private final Map<String, List<String>> useLegacyEventGenerationScheme;
private final Map<String, List<String>> disableUniqueEventStreamShapePreprocessing;
private final NamingStrategy namingStrategy;

public EventStreamUniqueEventShapesProcessor(
Map<String, List<String>> useLegacyEventGenerationScheme,
Map<String, List<String>> disableUniqueEventStreamShapePreprocessing,
NamingStrategy namingStrategy) {
this.useLegacyEventGenerationScheme = useLegacyEventGenerationScheme;
this.disableUniqueEventStreamShapePreprocessing = disableUniqueEventStreamShapePreprocessing;
this.namingStrategy = namingStrategy;
}

@Override
public void preprocess(ServiceModel serviceModel) {
Map<String, Shape> newEventShapes = new HashMap<>();
serviceModel.getShapes().forEach((name, shape) -> {
if (!shape.isEventstream()) {
return;
}

preprocessEventStream(serviceModel, name, shape, newEventShapes);
});
serviceModel.getShapes().putAll(newEventShapes);
}

private void preprocessEventStream(ServiceModel serviceModel, String eventStreamName, Shape eventStreamShape, Map<String,
Shape> newEventShapes) {
Set<String> disableUniqueEventDuplication =
Stream.of(
useLegacyEventGenerationScheme.getOrDefault(eventStreamName, Collections.emptyList()),
disableUniqueEventStreamShapePreprocessing.getOrDefault(eventStreamName, Collections.emptyList())
)
.flatMap(List::stream)
.collect(Collectors.toSet());

eventStreamShape.getMembers().forEach((memberName, member) -> {
String eventShapeName = member.getShape();
Shape memberTargetShape = serviceModel.getShape(eventShapeName);

if (memberTargetShape.isEvent() && !disableUniqueEventDuplication.contains(memberName)) {
String newShapeName = namingStrategy.getUniqueEventStreamEventShapeName(member, eventStreamName);
if (serviceModel.getShapes().containsKey(newShapeName)) {
log.warn(() -> String.format("Shape name conflict, unable to create a new unique event shape name for %s in"
+ " eventstream %s because %s already exists in the model. Skipping.",
eventShapeName, eventStreamName, newShapeName));
} else {
log.debug(() -> String.format("Creating new, unique, event shape for %s in eventstream %s: %s",
eventShapeName, eventStreamName, newShapeName));
newEventShapes.put(newShapeName, memberTargetShape);
member.setShape(newShapeName);
}
}
});
}

@Override
public void postprocess(IntermediateModel intermediateModel) {
// no-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ public class CustomizationConfig {
*/
private Map<String, List<String>> useLegacyEventGenerationScheme = new HashMap<>();

/**
* Customization to instruct the code generator to not duplicate event shapes to ensure that event stream shapes
* are unique
* <p>
* <b>NOTE</b>This customization is primarily here to preserve backwards compatibility with existing code before the
* EventStreamUniqueEventShapesProcessor was added.
*/
private Map<String, List<String>> disableUniqueEventStreamShapePreprocessing = new HashMap<>();

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

public Map<String, List<String>> getDisableUniqueEventStreamShapePreprocessing() {
return disableUniqueEventStreamShapePreprocessing;
}

public void setDisableUniqueEventStreamShapePreprocessing(
Map<String, List<String>> disableUniqueEventStreamShapePreprocessing) {
this.disableUniqueEventStreamShapePreprocessing = disableUniqueEventStreamShapePreprocessing;
}

public UnderscoresInNameBehavior getUnderscoresInNameBehavior() {
return underscoresInNameBehavior;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.MemberModel;
import software.amazon.awssdk.codegen.model.intermediate.Metadata;
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.utils.Logger;
Expand Down Expand Up @@ -411,6 +412,12 @@ public String getUnionEnumTypeName(MemberModel memberModel) {
return screamCase(memberModel.getName());
}

@Override
public String getUniqueEventStreamEventShapeName(Member eventMember, String eventStreamName) {
return eventStreamName + eventMember.getShape();
}


private String rewriteInvalidMemberName(String memberName, Shape parentShape) {
if (isJavaKeyword(memberName) || isDisallowedNameForShape(memberName, parentShape)) {
return Utils.unCapitalize(memberName + CONFLICTING_NAME_SUFFIX);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.MemberModel;
import software.amazon.awssdk.codegen.model.service.Member;
import software.amazon.awssdk.codegen.model.service.Shape;
import software.amazon.awssdk.core.SdkField;

Expand Down Expand Up @@ -200,6 +201,15 @@ public interface NamingStrategy {
*/
String getExistenceCheckMethodName(String memberName, Shape parentShape);

/**
* Returns a unique shape name to use for an event member of an eventStream.
*
* @param eventMember The event member to get the shape name for.
* @param eventStreamName The name of the eventStream containing the member.
* @return Unique name for the event shape / eventStream combo.
*/
String getUniqueEventStreamEventShapeName(Member eventMember, String eventStreamName);

/**
* Verify the customer-visible naming in the provided intermediate model will compile and is idiomatic to Java.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


import java.io.File;
import org.junit.BeforeClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
Expand All @@ -39,11 +39,11 @@ public class UseLegacyEventGenerationSchemeProcessorTest {
@Rule
public ExpectedException thrown = ExpectedException.none();

private static ServiceModel serviceModel;
private ServiceModel serviceModel;


@BeforeClass
public static void setup() {
@Before
public void setup() {
String c2jFilePath = UseLegacyEventGenerationSchemeProcessorTest.class.getResource(RESOURCE_ROOT + "/service-2.json").getFile();
File c2jFile = new File(c2jFilePath);

Expand All @@ -67,7 +67,7 @@ public void testPostProcess_customizationIsValid_succeeds() {
}


private static IntermediateModel intermediateModelWithConfig(String configName) {
private IntermediateModel intermediateModelWithConfig(String configName) {
return new IntermediateModelBuilder(C2jModels.builder()
.serviceModel(serviceModel)
.customizationConfig(loadCustomizationConfig(configName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Locale;
import org.junit.Test;
Expand Down Expand Up @@ -52,15 +51,15 @@ public SharedStreamAwsModelSpecTest(ShapeModel shapeModel) {
}

@Test
public void basicGeneration() throws Exception {
public void basicGeneration() {
assertThat(new AwsServiceModel(intermediateModel, shapeModel), generatesTo(referenceFileForShape()));
}

private String referenceFileForShape() {
return "sharedstream/" + shapeModel.getShapeName().toLowerCase(Locale.ENGLISH) + ".java";
}

private static void setUp() throws IOException {
private static void setUp() {
File serviceModelFile = new File(SharedStreamAwsModelSpecTest.class.getResource("sharedstream/service-2.json").getFile());
ServiceModel serviceModel = ModelLoaderUtils.loadModel(ServiceModel.class, serviceModelFile);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package software.amazon.awssdk.codegen.poet.model;

import static java.util.stream.Collectors.toList;
import static org.hamcrest.MatcherAssert.assertThat;
import static software.amazon.awssdk.codegen.poet.PoetMatchers.generatesTo;
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
import java.io.File;
import java.util.Collection;
import java.util.Locale;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
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.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.MemberModel;
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
import software.amazon.awssdk.codegen.model.service.ServiceModel;
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils;

@RunWith(Parameterized.class)
public class SharedStreamEventModelSpecTest {
private static IntermediateModel intermediateModel;

private final MemberModel event;
private final ShapeModel eventStream;

@Parameterized.Parameters(name = "{1}-{0}")
public static Collection<Object[]> data() {
invokeSafely(SharedStreamEventModelSpecTest::setUp);
return intermediateModel.getShapes().values().stream()
.filter(ShapeModel::isEventStream)
.flatMap(eventStream -> eventStream.getMembers().stream()
.filter(m -> m.getShape().isEvent())
.map(e -> new Object[]{e, eventStream}))
.collect(toList());
}

public SharedStreamEventModelSpecTest(MemberModel event, ShapeModel eventStream) {
this.event = event;
this.eventStream = eventStream;
}

@Test
public void basicGeneration() {
assertThat(new EventModelSpec(event, eventStream, intermediateModel), generatesTo(referenceFileForShape()));
}

private String referenceFileForShape() {
String namespacedEventImpl = eventStream.getShapeName() + "/default" + event.getName();
return "sharedstream/" + namespacedEventImpl.toLowerCase(Locale.ENGLISH) + ".java";
}

private static void setUp() {
File serviceModelFile = new File(SharedStreamEventModelSpecTest.class.getResource("sharedstream/service-2.json").getFile());
ServiceModel serviceModel = ModelLoaderUtils.loadModel(ServiceModel.class, serviceModelFile);

intermediateModel = new IntermediateModelBuilder(
C2jModels.builder()
.serviceModel(serviceModel)
.customizationConfig(CustomizationConfig.create())
.build())
.build();
}
}
Loading
Loading