From 036a5a39ffb553ea619b7d62cf1f0884e4d65b25 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 19 Aug 2026 14:15:32 -0400 Subject: [PATCH 01/44] Smithy: per-service model transforms and awsQueryCompatible ResponseMetadata Add per-service Smithy model transforms (SourceRegion, Lambda, SQS, ApiGateway, ApiGatewayV2, EC2) and a shared TransformSupport helper, and wire them into the ModelCodegenPlugin transform pipeline. Extend GlobalTransforms to also inject the ResponseMetadata envelope for awsQueryCompatible services (e.g. SQS = awsJson1_0 + @awsQueryCompatible), matching legacy C2J CppClientGenerator.addRequestIdToResults. ResponseMetadata is now reserved via a shared constant and injection fails fast on any modeled collision rather than silently mis-generating. --- .../generators/model/ModelCodegenPlugin.java | 16 +- .../transforms/ApiGatewayTransforms.java | 54 ++++++ .../transforms/ApiGatewayV2Transforms.java | 46 +++++ .../model/transforms/Ec2Transforms.java | 148 ++++++++++++++++ .../model/transforms/GlobalTransforms.java | 45 +++-- .../model/transforms/LambdaTransforms.java | 57 +++++++ .../transforms/SourceRegionTransform.java | 85 ++++++++++ .../model/transforms/SqsTransforms.java | 49 ++++++ .../model/transforms/TransformSupport.java | 99 +++++++++++ .../model/GlobalTransformsTest.java | 146 +++++++++++++++- .../transforms/ApiGatewayTransformsTest.java | 75 +++++++++ .../ApiGatewayV2TransformsTest.java | 63 +++++++ .../model/transforms/Ec2TransformsTest.java | 158 ++++++++++++++++++ .../transforms/LambdaTransformsTest.java | 61 +++++++ .../transforms/SourceRegionTransformTest.java | 100 +++++++++++ .../model/transforms/SqsTransformsTest.java | 91 ++++++++++ 16 files changed, 1277 insertions(+), 16 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index d3eeee70276..a9066c5fd9d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -6,7 +6,13 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayTransforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayV2Transforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.Ec2Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LambdaTransforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SourceRegionTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SqsTransforms; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; import software.amazon.smithy.model.Model; @@ -42,8 +48,14 @@ public void execute(PluginContext context) { // Build transform pipeline (service-level transforms will be registered here) TransformPipeline pipeline = new TransformPipeline(List.of( - GlobalTransforms.asTransform() - // Future: S3Transforms.asTransform(), Ec2Transforms.asTransform(), etc. + GlobalTransforms.asTransform(), + SourceRegionTransform.asTransform(), + LambdaTransforms.asTransform(), + SqsTransforms.asTransform(), + ApiGatewayTransforms.asTransform(), + ApiGatewayV2Transforms.asTransform(), + Ec2Transforms.asTransform() + // Future: S3Transforms.asTransform(), etc. )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java new file mode 100644 index 00000000000..221cadd326a --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java @@ -0,0 +1,54 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renames the reserved {@code body}/{@code headers} members of API Gateway's test-invoke requests to + * {@code requestBody}/{@code requestHeaders}. Mirrors the legacy C2J {@code APIGatewayRestJsonCppClientGenerator}. + */ +public final class ApiGatewayTransforms { + + private ApiGatewayTransforms() {} + + public static ModelTransform asTransform() { + return ApiGatewayTransforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"api-gateway".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + String ns = service.getId().getNamespace(); + List updated = new ArrayList<>(); + for (String requestName : List.of("TestInvokeMethodRequest", "TestInvokeAuthorizerRequest")) { + model.getShape(ShapeId.fromParts(ns, requestName)) + .flatMap(s -> s.asStructureShape()) + .ifPresent(struct -> { + StructureShape afterBody = TransformSupport + .renameMember(struct, "body", "requestBody").orElse(struct); + TransformSupport.renameMember(afterBody, "headers", "requestHeaders") + .ifPresentOrElse(updated::add, () -> { + if (afterBody != struct) { + updated.add(afterBody); + } + }); + }); + } + if (updated.isEmpty()) { + return model; + } + return model.toBuilder().addShapes(updated).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java new file mode 100644 index 00000000000..afbb391a458 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java @@ -0,0 +1,46 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renames the reserved {@code Body} member of API Gateway V2's import requests to {@code requestBody}. + * Mirrors the legacy C2J {@code APIGatewayV2RestJsonCppClientGenerator}. + */ +public final class ApiGatewayV2Transforms { + + private ApiGatewayV2Transforms() {} + + public static ModelTransform asTransform() { + return ApiGatewayV2Transforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"apigatewayv2".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + String ns = service.getId().getNamespace(); + List updated = new ArrayList<>(); + for (String requestName : List.of("ImportApiRequest", "ReimportApiRequest")) { + model.getShape(ShapeId.fromParts(ns, requestName)) + .flatMap(s -> s.asStructureShape()) + .flatMap(struct -> TransformSupport.renameMember(struct, "Body", "requestBody")) + .ifPresent(updated::add); + } + if (updated.isEmpty()) { + return model; + } + return model.toBuilder().addShapes(updated).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java new file mode 100644 index 00000000000..6a007880881 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java @@ -0,0 +1,148 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.model.transform.ModelTransformer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * EC2 model parity with the legacy C2J {@code Ec2CppClientGenerator}: adds the unmodeled + * {@code disabled} value to {@code SpotInstanceState}, renames every {@code *Result} + * structure shape to {@code *Response} so nested domain structs (e.g. {@code MetricDataResult}) + * match C2J, and models {@code ModifyInstanceAttributeRequest.UserData} as the sensitive + * {@code SecureBlobAttributeValue} to match the C2J model. Operation-OUTPUT result files are + * handled centrally by {@code ShapeUtil.getResultSuffix}, but nested domain structs are rendered + * from the shape name by {@code SubObjectRenderer}, so those require a model-shape rename. Out of + * scope (client/endpoint path, left to C2J): the legacy error-code injection, CopySnapshot + * pre-signing, and endpoint template. + * + *

UserData / SecureBlobAttributeValue: the upstream {@code aws/aws-models} C2J model + * ({@code ec2//service-2.json}) marks {@code UserData} sensitive via + * {@code SecureBlobAttributeValue -> SecureBlob (@sensitive)}, but the upstream Smithy model + * ({@code ec2/smithy/model.json}) still targets the non-sensitive {@code BlobAttributeValue}. This + * transform mirrors the C2J modeling in the Smithy model so generated code matches. It self-retires + * (no-op) once the upstream Smithy model catches up, and is a temporary compensation for that + * upstream data lag — see docs/superpowers/plans/parity-deltas.md. + */ +public final class Ec2Transforms { + + private Ec2Transforms() {} + + public static ModelTransform asTransform() { + return Ec2Transforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"ec2".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + return renameResultShapesToResponse( + addSecureBlobUserData(addSpotInstanceStateDisabled(model))); + } + + /** + * Models {@code ModifyInstanceAttributeRequest.UserData} as {@code SecureBlobAttributeValue} + * (whose {@code Value} member targets a {@code @sensitive} {@code SecureBlob} blob), matching + * the C2J model. The upstream Smithy model still targets the non-sensitive + * {@code BlobAttributeValue}; after repointing, {@code BlobAttributeValue} is no longer + * referenced and drops out of the reachable (emitted) set, exactly as it does in C2J. + * + *

No-op — leaving the model untouched — when {@code SecureBlobAttributeValue} already exists + * (upstream Smithy caught up) or {@code UserData} no longer targets {@code BlobAttributeValue}, + * so the transform cannot introduce a duplicate shape or fight a corrected upstream model. + */ + private static Model addSecureBlobUserData(Model model) { + Optional requestOpt = model.shapes(StructureShape.class) + .filter(s -> "ModifyInstanceAttributeRequest".equals(s.getId().getName())) + .findFirst(); + if (requestOpt.isEmpty()) { + return model; + } + StructureShape request = requestOpt.get(); + MemberShape userData = request.getAllMembers().get("UserData"); + if (userData == null) { + return model; + } + + String namespace = request.getId().getNamespace(); + ShapeId secureBlobId = ShapeId.fromParts(namespace, "SecureBlob"); + ShapeId secureStructId = ShapeId.fromParts(namespace, "SecureBlobAttributeValue"); + ShapeId blobAttrId = ShapeId.fromParts(namespace, "BlobAttributeValue"); + + if (model.getShape(secureStructId).isPresent() || !userData.getTarget().equals(blobAttrId)) { + return model; + } + MemberShape originalValue = model.expectShape(blobAttrId, StructureShape.class) + .getAllMembers().get("Value"); + if (originalValue == null) { + return model; + } + + BlobShape secureBlob = BlobShape.builder() + .id(secureBlobId) + .addTrait(new SensitiveTrait()) + .build(); + // Copy BlobAttributeValue.Value's serde traits (ec2QueryName/xmlName), retargeting the blob. + MemberShape secureValue = originalValue.toBuilder() + .id(secureStructId.withMember("Value")) + .target(secureBlobId) + .build(); + StructureShape secureStruct = StructureShape.builder() + .id(secureStructId) + .addMember(secureValue) + .build(); + // Preserve UserData's own traits (ec2QueryName, documentation, xmlName); only retarget it. + MemberShape newUserData = userData.toBuilder().target(secureStructId).build(); + StructureShape newRequest = request.toBuilder().addMember(newUserData).build(); + + return model.toBuilder().addShapes(secureBlob, secureStruct, newRequest).build(); + } + + private static Model renameResultShapesToResponse(Model model) { + Map renames = new HashMap<>(); + for (StructureShape shape : model.shapes(StructureShape.class).toList()) { + String name = shape.getId().getName(); + if (name.endsWith("Result")) { + String target = name.substring(0, name.length() - "Result".length()) + "Response"; + ShapeId targetId = ShapeId.fromParts(shape.getId().getNamespace(), target); + if (!model.getShape(targetId).isPresent()) { + renames.put(shape.getId(), targetId); + } + } + } + if (renames.isEmpty()) { + return model; + } + return ModelTransformer.create().renameShapes(model, renames); + } + + private static Model addSpotInstanceStateDisabled(Model model) { + Optional enumShape = model.shapes() + .filter(s -> "SpotInstanceState".equals(s.getId().getName())) + .filter(s -> s.isEnumShape() || s.hasTrait(EnumTrait.class)) + .findFirst(); + if (enumShape.isEmpty()) { + return model; + } + return TransformSupport.appendValues(enumShape.get(), List.of("disabled")) + .map(updated -> model.toBuilder().addShape(updated).build()) + .orElse(model); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java index 587e926d047..926e51068f3 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java @@ -21,6 +21,7 @@ import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.model.transform.ModelTransformer; +import software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait; import java.util.ArrayList; import java.util.HashSet; @@ -51,6 +52,14 @@ public final class GlobalTransforms { "apigateway" ); + /** + * The framework-injected response-envelope member and shape name. It is reserved: no AWS model + * defines its own {@code ResponseMetadata}. {@link #injectResponseMetadata} adds it (and fails + * fast on any pre-existing collision), and {@code MemberRenderer} keys the "always-present" + * rendering (no {@code HasBeenSet} getter, flag initialized true) on this exact name. + */ + public static final String RESPONSE_METADATA = "ResponseMetadata"; + private GlobalTransforms() {} // NOTE: This reserved-member rename is intentionally NOT wired into the transform @@ -151,25 +160,38 @@ public static Model dropDeprecatedMembers(Model model, ServiceShape service) { } /** - * For awsQuery / ec2Query services, injects a {@code ResponseMetadata} structure (carrying + * For awsQuery / ec2Query services, and for any service carrying the + * {@code aws.protocols#awsQueryCompatible} trait (e.g. SQS = {@code awsJson1_0} + + * {@code @awsQueryCompatible}), injects a {@code ResponseMetadata} structure (carrying * a {@code RequestId} string member) and adds it as a {@code @required} member on every * result (operation output) shape. This mirrors the legacy C2J - * {@code QueryCppClientGenerator.addRequestIdToResults} injection, so that Query/EC2 result - * classes expose {@code GetResponseMetadata()} and back the {@code m_responseMetadata} - * deserialization emitted by {@code QueryXmlProtocolTraits}. Other protocols are unchanged. + * {@code CppClientGenerator.addRequestIdToResults} injection (which fires for query/ec2 + * protocols and, via its {@code awsQueryCompatible} branch, for awsQueryCompatible JSON + * services), so that those result classes expose {@code GetResponseMetadata()}. Other + * protocols are unchanged. * * @param model the current model * @param service the service being generated - * @return the model with ResponseMetadata injected, or the input model for non-query protocols + * @return the model with ResponseMetadata injected, or the input model for other protocols */ public static Model injectResponseMetadata(Model model, ServiceShape service) { Protocol protocol = ProtocolResolver.resolve(service, model); - if (protocol != Protocol.QUERY_XML && protocol != Protocol.EC2) { + boolean awsQueryCompatible = service.hasTrait(AwsQueryCompatibleTrait.class); + if (protocol != Protocol.QUERY_XML && protocol != Protocol.EC2 && !awsQueryCompatible) { return model; } String namespace = service.getId().getNamespace(); - ShapeId responseMetadataId = ShapeId.fromParts(namespace, "ResponseMetadata"); + ShapeId responseMetadataId = ShapeId.fromParts(namespace, RESPONSE_METADATA); + + // ResponseMetadata is reserved. If the model already defines a shape of that name, injecting + // ours would clobber it and MemberRenderer's name-based recognition could not tell them + // apart — fail fast rather than silently mis-generate. + if (model.getShape(responseMetadataId).isPresent()) { + throw new IllegalStateException("Service " + service.getId() + " already defines a shape '" + + responseMetadataId + "'; cannot inject the framework " + RESPONSE_METADATA + + " envelope"); + } // ResponseMetadata { RequestId: String } StructureShape responseMetadata = StructureShape.builder() @@ -192,12 +214,15 @@ public static Model injectResponseMetadata(Model model, ServiceShape service) { for (ShapeId outputId : outputIds) { model.getShape(outputId).flatMap(Shape::asStructureShape).ifPresent(result -> { - if (result.getMember("ResponseMetadata").isPresent()) { - return; + if (result.getMember(RESPONSE_METADATA).isPresent()) { + throw new IllegalStateException("Result shape " + result.getId() + + " already has a '" + RESPONSE_METADATA + "' member; cannot inject the " + + "framework " + RESPONSE_METADATA + " envelope. Rename the modeled member " + + "via a per-service transform first."); } StructureShape withMetadata = result.toBuilder() .addMember(MemberShape.builder() - .id(result.getId().withMember("ResponseMetadata")) + .id(result.getId().withMember(RESPONSE_METADATA)) .target(responseMetadataId) .addTrait(new RequiredTrait()) .build()) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java new file mode 100644 index 00000000000..344442d7543 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java @@ -0,0 +1,57 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.transform.ModelTransformer; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * Removes the deprecated {@code InvokeAsync} operation (and its input/output shapes) from the + * Lambda service. Mirrors the legacy C2J {@code LambdaRestJsonCppClientGenerator}, which removed + * {@code InvokeAsync} because it collides with the generated async client. + */ +public final class LambdaTransforms { + + private static final ShapeId UNIT = ShapeId.from("smithy.api#Unit"); + + private LambdaTransforms() {} + + public static ModelTransform asTransform() { + return LambdaTransforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"lambda".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + Optional invokeAsync = TopDownIndex.of(model) + .getContainedOperations(service).stream() + .filter(op -> "InvokeAsync".equals(op.getId().getName())) + .findFirst(); + if (invokeAsync.isEmpty()) { + return model; + } + OperationShape op = invokeAsync.get(); + Set toRemove = new HashSet<>(); + toRemove.add(op); + if (!UNIT.equals(op.getInputShape())) { + model.getShape(op.getInputShape()).ifPresent(toRemove::add); + } + op.getOutput().filter(id -> !UNIT.equals(id)) + .flatMap(model::getShape).ifPresent(toRemove::add); + return ModelTransformer.create().removeShapes(model, toRemove); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java new file mode 100644 index 00000000000..adac53b7a71 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java @@ -0,0 +1,85 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Injects a synthetic {@code SourceRegion} string member into the request shapes of the + * cross-region copy operations for RDS-family services. Mirrors the legacy C2J + * {@code RDSCppClientGenerator}/{@code DocDBCppClientGenerator}/{@code NeptuneCppClientGenerator} + * injection that backs presigned-URL generation. Model-shape scope only: the presigned-URL + * client logic remains in the C2J path, which references this member. + */ +public final class SourceRegionTransform { + + private static final String SOURCE_REGION = "SourceRegion"; + + // smithy service name (lowercase-hyphenated sdkId) -> operation names whose input gets SourceRegion. + private static final Map> TARGETS = Map.of( + "rds", Set.of( + "CopyDBClusterSnapshot", + "CreateDBCluster", + "CopyDBSnapshot", + "CreateDBInstanceReadReplica", + "StartDBInstanceAutomatedBackupsReplication"), + "docdb", Set.of( + "CopyDBClusterSnapshot", + "CreateDBCluster"), + "neptune", Set.of( + "CopyDBClusterSnapshot", + "CreateDBCluster") + ); + + private SourceRegionTransform() {} + + public static ModelTransform asTransform() { + return SourceRegionTransform::apply; + } + + private static Model apply(Model model, ServiceShape service) { + String serviceName = ServiceNameUtil.getSmithyServiceName(service, null); + Set operations = TARGETS.get(serviceName); + if (operations == null) { + return model; + } + + List updated = new ArrayList<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + if (!operations.contains(op.getId().getName())) { + continue; + } + model.getShape(op.getInputShape()).flatMap(s -> s.asStructureShape()).ifPresent(req -> { + if (req.getMember(SOURCE_REGION).isPresent()) { + return; + } + updated.add(req.toBuilder() + .addMember(MemberShape.builder() + .id(req.getId().withMember(SOURCE_REGION)) + .target(ShapeId.from("smithy.api#String")) + .build()) + .build()); + }); + } + + if (updated.isEmpty()) { + return model; + } + return model.toBuilder().addShapes(updated).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java new file mode 100644 index 00000000000..68d55f1c46e --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java @@ -0,0 +1,49 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.EnumTrait; + +import java.util.List; +import java.util.Optional; + +/** + * Adds the unmodeled {@code QueueAttributeName} enum values that the legacy C2J + * {@code SQSQueryXmlCppClientGenerator}/{@code SQSJsonCppClientGenerator} injected. These values are + * returned by the service but absent from the model. + */ +public final class SqsTransforms { + + private static final String ENUM_NAME = "QueueAttributeName"; + private static final List ADDED_VALUES = List.of( + "SentTimestamp", "ApproximateFirstReceiveTimestamp", "ApproximateReceiveCount", "SenderId"); + + private SqsTransforms() {} + + public static ModelTransform asTransform() { + return SqsTransforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"sqs".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + Optional target = model.shapes() + .filter(s -> ENUM_NAME.equals(s.getId().getName())) + .filter(s -> s.isEnumShape() || s.hasTrait(EnumTrait.class)) + .findFirst(); + if (target.isEmpty()) { + return model; + } + return TransformSupport.appendValues(target.get(), ADDED_VALUES) + .map(updated -> model.toBuilder().addShape(updated).build()) + .orElse(model); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java new file mode 100644 index 00000000000..c82fe57454d --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java @@ -0,0 +1,99 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.model.EnumRenderer; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; + +import java.util.List; +import java.util.Optional; + +/** + * Shared helpers for per-service model transforms. + */ +final class TransformSupport { + + /** + * Pattern for an identifier-safe Smithy enum member name: a leading letter or underscore + * followed by letters, digits, or underscores. + */ + private static final String IDENTIFIER_PATTERN = "[A-Za-z_][A-Za-z0-9_]*"; + + private TransformSupport() {} + + /** + * Appends the given wire {@code values} to an enum shape. + * + *

Precondition: each value MUST be an identifier-safe wire value, i.e. a + * valid Smithy enum member name matching {@code [A-Za-z_][A-Za-z0-9_]*}. Values containing + * characters such as {@code '-'}, {@code '.'}, or spaces are rejected. This precondition matters + * for two reasons: the idempotency dedup compares the incoming values against the shape's + * existing values (obtained via {@link EnumRenderer#getEnumValues(Shape)}), and the + * {@code EnumShape} branch uses each value directly as the Smithy member name via + * {@code builder.addMember(value, value)}. A non-identifier value would silently break dedup + * and fail deep inside Smithy, so it is rejected up front. + * + * @param enumShape the enum shape (Smithy 2.0 {@code EnumShape} or legacy {@code StringShape} + * with an {@code @enum} trait) to append to + * @param values identifier-safe wire values to append + * @return the updated shape, or {@link Optional#empty()} if all values are already present + * @throws IllegalArgumentException if any value is not an identifier-safe enum member name + */ + static Optional appendValues(Shape enumShape, List values) { + for (String value : values) { + if (value == null || !value.matches(IDENTIFIER_PATTERN)) { + throw new IllegalArgumentException( + "Enum value \"" + value + "\" for shape " + enumShape.getId() + + " is not an identifier-safe enum member name (must match " + + IDENTIFIER_PATTERN + ")"); + } + } + List existing = EnumRenderer.getEnumValues(enumShape); + List toAdd = values.stream().filter(v -> !existing.contains(v)).toList(); + if (toAdd.isEmpty()) { + return Optional.empty(); + } + if (enumShape.isEnumShape()) { + EnumShape.Builder builder = enumShape.asEnumShape().get().toBuilder(); + for (String value : toAdd) { + builder.addMember(value, value); + } + return Optional.of(builder.build()); + } + EnumTrait existingTrait = enumShape.expectTrait(EnumTrait.class); + EnumTrait.Builder traitBuilder = EnumTrait.builder(); + existingTrait.getValues().forEach(traitBuilder::addEnum); + for (String value : toAdd) { + traitBuilder.addEnum(EnumDefinition.builder().value(value).build()); + } + return Optional.of(enumShape.asStringShape().get().toBuilder() + .addTrait(traitBuilder.build()) + .build()); + } + + /** + * Returns a copy of {@code struct} with member {@code oldName} renamed to {@code newName}, + * preserving member declaration order and copying all traits onto the renamed member. Returns + * {@link Optional#empty()} if {@code oldName} is absent or {@code newName} already exists. + */ + static Optional renameMember(StructureShape struct, String oldName, String newName) { + if (struct.getMember(oldName).isEmpty() || struct.getMember(newName).isPresent()) { + return Optional.empty(); + } + StructureShape.Builder builder = StructureShape.builder().id(struct.getId()); + struct.getAllTraits().values().forEach(builder::addTrait); + for (MemberShape member : struct.getAllMembers().values()) { + String name = member.getMemberName().equals(oldName) ? newName : member.getMemberName(); + builder.addMember(name, member.getTarget(), + b -> member.getAllTraits().values().forEach(b::addTrait)); + } + return Optional.of(builder.build()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index d4be7bdda09..ffa9a1e1fdc 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java @@ -454,14 +454,102 @@ void dropDeprecatedMembers_orphanedTargetBecomesUnreachable() { "shape referenced only via a deprecated member must become unreachable"); } + @Test + void dropDeprecatedMembers_sharedTargetSurvivesViaNonDeprecatedReference() { + // A shape reached through BOTH a @deprecated member and a live member must stay reachable: + // dropping the deprecated reference must never orphan a shape the surviving model still uses. + // This guards against a pruning bug that would drop a shared shape and dangle the live ref. + StructureShape shared = StructureShape.builder() + .id("com.example#SharedDetail") + .addMember(MemberShape.builder() + .id("com.example#SharedDetail$x").target("smithy.api#String").build()) + .build(); + // Input references SharedDetail through a @deprecated member (dropped by the transform). + StructureShape input = StructureShape.builder() + .id("com.example#MyInput") + .addMember(MemberShape.builder() + .id("com.example#MyInput$legacyDetail").target(shared.getId()) + .addTrait(software.amazon.smithy.model.traits.DeprecatedTrait.builder().build()) + .build()) + .build(); + // Output references the same SharedDetail through a live (non-deprecated) member. + StructureShape output = StructureShape.builder() + .id("com.example#MyOutput") + .addMember(MemberShape.builder() + .id("com.example#MyOutput$detail").target(shared.getId()).build()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.example#MyOperation").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#MyService").version("2024-01-01").addOperation(op.getId()).build(); + Model model = Model.assembler() + .addShapes(shared, input, output, op, service).assemble().unwrap(); + + Model out = GlobalTransforms.dropDeprecatedMembers(model, service); + StructureShape transformedInput = out.expectShape( + ShapeId.from("com.example#MyInput"), StructureShape.class); + assertFalse(transformedInput.getMember("legacyDetail").isPresent(), + "deprecated reference must be dropped from its container"); + + Set reachable = GlobalTransforms.computeReachableShapes(out, serviceOf(out, "MyService")); + assertTrue(reachable.contains(ShapeId.from("com.example#SharedDetail")), + "shape still referenced by a surviving member must remain reachable (and emitted)"); + } + private static ServiceShape serviceOf(Model model, String name) { return model.expectShape(ShapeId.from("com.example#" + name), ServiceShape.class); } + @Test + void injectResponseMetadata_failsFastOnModeledResponseMetadataMember() { + // ResponseMetadata is framework-reserved. A modeled member of that name on a result would + // make MemberRenderer's name-based recognition ambiguous, so injection fails fast rather + // than clobber the modeled member or silently mis-render it. + StructureShape input = StructureShape.builder().id("com.example#DoThingInput").build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoThingOutput") + .addMember(MemberShape.builder() + .id("com.example#DoThingOutput$ResponseMetadata").target("smithy.api#String").build()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoThing").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(new software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait()) + .addOperation(op.getId()).build(); + Model model = Model.assembler().addShapes(input, output, op, service).assemble().unwrap(); + + assertThrows(IllegalStateException.class, + () -> GlobalTransforms.injectResponseMetadata(model, serviceOf(model, "Example"))); + } + + @Test + void injectResponseMetadata_failsFastOnModeledResponseMetadataShape() { + // A modeled shape literally named ResponseMetadata collides with the framework envelope + // shape we create; injecting would clobber it, so fail fast. + StructureShape input = StructureShape.builder().id("com.example#DoThingInput").build(); + StructureShape output = StructureShape.builder().id("com.example#DoThingOutput").build(); + StructureShape modeled = StructureShape.builder() + .id("com.example#ResponseMetadata") + .addMember(MemberShape.builder() + .id("com.example#ResponseMetadata$foo").target("smithy.api#String").build()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoThing").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(new software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait()) + .addOperation(op.getId()).build(); + Model model = Model.assembler().addShapes(input, output, modeled, op, service).assemble().unwrap(); + + assertThrows(IllegalStateException.class, + () -> GlobalTransforms.injectResponseMetadata(model, serviceOf(model, "Example"))); + } + // --- injectResponseMetadata tests --- - /** A single-operation service under the given protocol trait, output has one plain member. */ - private static Model oneOutputModel(software.amazon.smithy.model.traits.Trait protocolTrait) { + /** A single-operation service under the given protocol trait(s), output has one plain member. */ + private static Model oneOutputModel(software.amazon.smithy.model.traits.Trait... serviceTraits) { StructureShape input = StructureShape.builder().id("com.example#DoThingInput").build(); StructureShape output = StructureShape.builder() .id("com.example#DoThingOutput") @@ -470,9 +558,13 @@ private static Model oneOutputModel(software.amazon.smithy.model.traits.Trait pr .build(); OperationShape op = OperationShape.builder() .id("com.example#DoThing").input(input.getId()).output(output.getId()).build(); - ServiceShape service = ServiceShape.builder() + ServiceShape.Builder serviceBuilder = ServiceShape.builder() .id("com.example#Example").version("2024-01-01") - .addTrait(protocolTrait).addOperation(op.getId()).build(); + .addOperation(op.getId()); + for (software.amazon.smithy.model.traits.Trait trait : serviceTraits) { + serviceBuilder.addTrait(trait); + } + ServiceShape service = serviceBuilder.build(); return Model.assembler().addShapes(input, output, op, service).assemble().unwrap(); } @@ -529,4 +621,50 @@ void injectResponseMetadata_restJson_leavesResultUnchanged() { assertFalse(result.getMember("ResponseMetadata").isPresent(), "Non-query protocols must not get ResponseMetadata injected"); } + + @Test + void injectResponseMetadata_awsJsonWithQueryCompatible_addsResponseMetadataMemberToResult() { + // awsJson1_0 + @awsQueryCompatible (e.g. SQS) resolves to a JSON protocol, but C2J still + // injects ResponseMetadata { RequestId } into every result for awsQueryCompatible services. + Model model = oneOutputModel( + software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait.builder().build(), + new software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait()); + Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + + StructureShape result = out.expectShape( + ShapeId.from("com.example#DoThingOutput"), StructureShape.class); + assertTrue(result.getMember("ResponseMetadata").isPresent(), + "awsQueryCompatible JSON result should carry an injected ResponseMetadata member"); + MemberShape rm = result.getMember("ResponseMetadata").get(); + assertTrue(rm.hasTrait(software.amazon.smithy.model.traits.RequiredTrait.class), + "ResponseMetadata member should be @required, matching C2J"); + } + + @Test + void injectResponseMetadata_awsJsonWithQueryCompatible_addsResponseMetadataStructureWithRequestId() { + Model model = oneOutputModel( + software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait.builder().build(), + new software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait()); + Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + + ShapeId rmId = out.expectShape(ShapeId.from("com.example#DoThingOutput"), StructureShape.class) + .getMember("ResponseMetadata").get().getTarget(); + StructureShape rm = out.expectShape(rmId, StructureShape.class); + assertEquals("ResponseMetadata", rmId.getName()); + assertTrue(rm.getMember("RequestId").isPresent(), + "ResponseMetadata should have a RequestId member"); + } + + @Test + void injectResponseMetadata_awsJsonWithoutQueryCompatible_leavesResultUnchanged() { + // Plain awsJson1_0 (no @awsQueryCompatible) must NOT get ResponseMetadata injected. + Model model = oneOutputModel( + software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait.builder().build()); + Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + + StructureShape result = out.expectShape( + ShapeId.from("com.example#DoThingOutput"), StructureShape.class); + assertFalse(result.getMember("ResponseMetadata").isPresent(), + "Plain JSON protocols must not get ResponseMetadata injected"); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java new file mode 100644 index 00000000000..ddacd6fe035 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java @@ -0,0 +1,75 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; + +import static org.junit.jupiter.api.Assertions.*; + +class ApiGatewayTransformsTest { + + private static Model apiGatewayModel(String sdkId) { + StructureShape testInvokeMethod = StructureShape.builder() + .id("com.example#TestInvokeMethodRequest") + .addMember(MemberShape.builder().id("com.example#TestInvokeMethodRequest$body") + .target("smithy.api#String").build()) + .addMember(MemberShape.builder().id("com.example#TestInvokeMethodRequest$headers") + .target("smithy.api#String").build()) + .build(); + StructureShape testInvokeAuth = StructureShape.builder() + .id("com.example#TestInvokeAuthorizerRequest") + .addMember(MemberShape.builder().id("com.example#TestInvokeAuthorizerRequest$body") + .target("smithy.api#String").build()) + .addMember(MemberShape.builder().id("com.example#TestInvokeAuthorizerRequest$headers") + .target("smithy.api#String").build()) + .build(); + StructureShape out = StructureShape.builder().id("com.example#EmptyOut").build(); + OperationShape op1 = OperationShape.builder().id("com.example#TestInvokeMethod") + .input(testInvokeMethod.getId()).output(out.getId()).build(); + OperationShape op2 = OperationShape.builder().id("com.example#TestInvokeAuthorizer") + .input(testInvokeAuth.getId()).output(out.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("apigateway") + .cloudFormationName("ApiGateway").cloudTrailEventSource("apigateway").build()) + .addOperation(op1.getId()).addOperation(op2.getId()) + .build(); + return Model.assembler() + .addShapes(testInvokeMethod, testInvokeAuth, out, op1, op2, service) + .assemble().unwrap(); + } + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + @Test + void renamesBodyAndHeaders() { + Model m = apiGatewayModel("API Gateway"); + Model out = ApiGatewayTransforms.asTransform().apply(m, service(m)); + + StructureShape r = out.expectShape( + ShapeId.from("com.example#TestInvokeMethodRequest"), StructureShape.class); + assertTrue(r.getMember("requestBody").isPresent()); + assertTrue(r.getMember("requestHeaders").isPresent()); + assertTrue(r.getMember("body").isEmpty()); + assertTrue(r.getMember("headers").isEmpty()); + + StructureShape a = out.expectShape( + ShapeId.from("com.example#TestInvokeAuthorizerRequest"), StructureShape.class); + assertTrue(a.getMember("requestBody").isPresent()); + assertTrue(a.getMember("requestHeaders").isPresent()); + } + + @Test + void noOpForOtherService() { + Model m = apiGatewayModel("SomeOther"); + Model out = ApiGatewayTransforms.asTransform().apply(m, service(m)); + assertSame(m, out); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java new file mode 100644 index 00000000000..82dcb27db15 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java @@ -0,0 +1,63 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; + +import static org.junit.jupiter.api.Assertions.*; + +class ApiGatewayV2TransformsTest { + + private static StructureShape reqWithBody(String name) { + return StructureShape.builder() + .id("com.example#" + name) + .addMember(MemberShape.builder().id("com.example#" + name + "$Body") + .target("smithy.api#String").build()) + .build(); + } + + private static Model model(String sdkId) { + StructureShape importApi = reqWithBody("ImportApiRequest"); + StructureShape reimportApi = reqWithBody("ReimportApiRequest"); + StructureShape out = StructureShape.builder().id("com.example#EmptyOut").build(); + OperationShape op1 = OperationShape.builder().id("com.example#ImportApi") + .input(importApi.getId()).output(out.getId()).build(); + OperationShape op2 = OperationShape.builder().id("com.example#ReimportApi") + .input(reimportApi.getId()).output(out.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("apigatewayv2") + .cloudFormationName("ApiGatewayV2").cloudTrailEventSource("apigatewayv2").build()) + .addOperation(op1.getId()).addOperation(op2.getId()) + .build(); + return Model.assembler().addShapes(importApi, reimportApi, out, op1, op2, service) + .assemble().unwrap(); + } + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + @Test + void renamesBody() { + Model m = model("ApiGatewayV2"); + Model out = ApiGatewayV2Transforms.asTransform().apply(m, service(m)); + for (String name : new String[]{"ImportApiRequest", "ReimportApiRequest"}) { + StructureShape r = out.expectShape(ShapeId.from("com.example#" + name), StructureShape.class); + assertTrue(r.getMember("requestBody").isPresent(), name); + assertTrue(r.getMember("Body").isEmpty(), name); + } + } + + @Test + void noOpForOtherService() { + Model m = model("SomeOther"); + Model out = ApiGatewayV2Transforms.asTransform().apply(m, service(m)); + assertSame(m, out); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java new file mode 100644 index 00000000000..0dbe9184ba3 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java @@ -0,0 +1,158 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.model.EnumRenderer; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.SensitiveTrait; + +import static org.junit.jupiter.api.Assertions.*; + +class Ec2TransformsTest { + + private static Model ec2Model(String sdkId) { + StructureShape in = StructureShape.builder().id("com.example#DescribeThingsRequest").build(); + StructureShape out = StructureShape.builder().id("com.example#DescribeThingsResult").build(); + OperationShape op = OperationShape.builder().id("com.example#DescribeThings") + .input(in.getId()).output(out.getId()).build(); + EnumShape spot = EnumShape.builder().id("com.example#SpotInstanceState") + .addMember("open", "open").addMember("active", "active").build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("ec2") + .cloudFormationName("EC2").cloudTrailEventSource("ec2").build()) + .addOperation(op.getId()) + .build(); + return Model.assembler().addShapes(in, out, op, spot, service).assemble().unwrap(); + } + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + private static ServiceShape ec2Service(String sdkId) { + return ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("ec2") + .cloudFormationName("EC2").cloudTrailEventSource("ec2").build()) + .build(); + } + + @Test + void addsDisabledToSpotInstanceState() { + Model m = ec2Model("EC2"); + Model out = Ec2Transforms.asTransform().apply(m, service(m)); + assertTrue(EnumRenderer.getEnumValues( + out.expectShape(ShapeId.from("com.example#SpotInstanceState"))).contains("disabled")); + } + + @Test + void noOpForOtherService() { + Model m = ec2Model("SomeOther"); + Model out = Ec2Transforms.asTransform().apply(m, service(m)); + assertSame(m, out); + } + + @Test + void renamesNestedResultStructToResponse() { + StructureShape nested = StructureShape.builder() + .id("com.example#MetricDataResult").build(); + ServiceShape service = ec2Service("EC2"); + Model m = Model.assembler().addShapes(nested, service).assemble().unwrap(); + + Model out = Ec2Transforms.asTransform().apply(m, service); + + assertFalse(out.getShape(ShapeId.from("com.example#MetricDataResult")).isPresent()); + assertTrue(out.getShape(ShapeId.from("com.example#MetricDataResponse")).isPresent()); + } + + @Test + void collisionGuardLeavesResultUnchangedWhenResponseExists() { + StructureShape resultShape = StructureShape.builder() + .id("com.example#FooResult").build(); + StructureShape responseShape = StructureShape.builder() + .id("com.example#FooResponse").build(); + ServiceShape service = ec2Service("EC2"); + Model m = Model.assembler().addShapes(resultShape, responseShape, service).assemble().unwrap(); + + Model out = Ec2Transforms.asTransform().apply(m, service); + + assertTrue(out.getShape(ShapeId.from("com.example#FooResult")).isPresent()); + assertTrue(out.getShape(ShapeId.from("com.example#FooResponse")).isPresent()); + } + + /** + * Model mirroring the upstream Smithy EC2 shape: {@code ModifyInstanceAttributeRequest.UserData} + * targets the non-sensitive {@code BlobAttributeValue { Value: Blob }}. + */ + private static Model userDataModel() { + BlobShape blob = BlobShape.builder().id("com.example#Blob").build(); + StructureShape blobAttr = StructureShape.builder() + .id("com.example#BlobAttributeValue") + .addMember(MemberShape.builder() + .id("com.example#BlobAttributeValue$Value").target(blob.getId()).build()) + .build(); + StructureShape request = StructureShape.builder() + .id("com.example#ModifyInstanceAttributeRequest") + .addMember(MemberShape.builder() + .id("com.example#ModifyInstanceAttributeRequest$UserData").target(blobAttr.getId()).build()) + .build(); + StructureShape out = StructureShape.builder().id("com.example#ModifyInstanceAttributeResult").build(); + OperationShape op = OperationShape.builder().id("com.example#ModifyInstanceAttribute") + .input(request.getId()).output(out.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId("EC2").arnNamespace("ec2") + .cloudFormationName("EC2").cloudTrailEventSource("ec2").build()) + .addOperation(op.getId()) + .build(); + return Model.assembler().addShapes(blob, blobAttr, request, out, op, service).assemble().unwrap(); + } + + @Test + void modelsUserDataAsSensitiveSecureBlobAttributeValue() { + Model m = userDataModel(); + Model out = Ec2Transforms.asTransform().apply(m, service(m)); + + // SecureBlobAttributeValue exists with a Value member targeting a @sensitive blob + // (a @sensitive blob maps to Aws::Utils::CryptoBuffer, matching the C2J baseline). + StructureShape secure = out.expectShape( + ShapeId.from("com.example#SecureBlobAttributeValue"), StructureShape.class); + MemberShape value = secure.getAllMembers().get("Value"); + assertNotNull(value, "SecureBlobAttributeValue must have a Value member"); + Shape valueTarget = out.expectShape(value.getTarget()); + assertTrue(valueTarget.isBlobShape(), "Value must target a blob"); + assertTrue(valueTarget.hasTrait(SensitiveTrait.class), + "the blob must be @sensitive so it renders as CryptoBuffer"); + + // UserData is repointed to SecureBlobAttributeValue. + MemberShape userData = out.expectShape( + ShapeId.from("com.example#ModifyInstanceAttributeRequest"), StructureShape.class) + .getAllMembers().get("UserData"); + assertEquals(ShapeId.from("com.example#SecureBlobAttributeValue"), userData.getTarget()); + + // BlobAttributeValue is now unreferenced, so it drops out of the emitted set (matching C2J). + boolean stillReferenced = out.shapes(StructureShape.class) + .flatMap(s -> s.getAllMembers().values().stream()) + .anyMatch(mem -> mem.getTarget().equals(ShapeId.from("com.example#BlobAttributeValue"))); + assertFalse(stillReferenced, "BlobAttributeValue must be unreferenced after repointing UserData"); + } + + @Test + void secureBlobUserDataTransformIsIdempotent() { + // Re-applying must not throw or duplicate shapes: once SecureBlobAttributeValue exists the + // transform self-retires, so it is safe if the upstream Smithy model later adds the shape. + Model once = Ec2Transforms.asTransform().apply(userDataModel(), service(userDataModel())); + Model twice = Ec2Transforms.asTransform().apply(once, service(once)); + + MemberShape userData = twice.expectShape( + ShapeId.from("com.example#ModifyInstanceAttributeRequest"), StructureShape.class) + .getAllMembers().get("UserData"); + assertEquals(ShapeId.from("com.example#SecureBlobAttributeValue"), userData.getTarget()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java new file mode 100644 index 00000000000..49c31744cf5 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java @@ -0,0 +1,61 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; + +import static org.junit.jupiter.api.Assertions.*; + +class LambdaTransformsTest { + + private static Model lambdaModel(String sdkId) { + StructureShape invokeReq = StructureShape.builder().id("com.example#InvocationRequest").build(); + StructureShape invokeRes = StructureShape.builder().id("com.example#InvocationResponse").build(); + OperationShape invoke = OperationShape.builder() + .id("com.example#Invoke").input(invokeReq.getId()).output(invokeRes.getId()).build(); + + StructureShape asyncReq = StructureShape.builder().id("com.example#InvokeAsyncRequest").build(); + StructureShape asyncRes = StructureShape.builder().id("com.example#InvokeAsyncResult").build(); + OperationShape invokeAsync = OperationShape.builder() + .id("com.example#InvokeAsync").input(asyncReq.getId()).output(asyncRes.getId()).build(); + + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace(sdkId.toLowerCase()) + .cloudFormationName(sdkId).cloudTrailEventSource(sdkId.toLowerCase()).build()) + .addOperation(invoke.getId()).addOperation(invokeAsync.getId()) + .build(); + return Model.assembler() + .addShapes(invokeReq, invokeRes, invoke, asyncReq, asyncRes, invokeAsync, service) + .assemble().unwrap(); + } + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + @Test + void removesInvokeAsyncOperationAndShapes() { + Model m = lambdaModel("Lambda"); + Model out = LambdaTransforms.asTransform().apply(m, service(m)); + + assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsync")).isEmpty()); + assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsyncRequest")).isEmpty()); + assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsyncResult")).isEmpty()); + // Invoke and its shapes are untouched + assertTrue(out.getShape(ShapeId.from("com.example#Invoke")).isPresent()); + assertTrue(out.getShape(ShapeId.from("com.example#InvocationRequest")).isPresent()); + } + + @Test + void noOpForOtherService() { + Model m = lambdaModel("SomeOther"); + Model out = LambdaTransforms.asTransform().apply(m, service(m)); + assertSame(m, out); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java new file mode 100644 index 00000000000..5993373c131 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java @@ -0,0 +1,100 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; + +import static org.junit.jupiter.api.Assertions.*; + +class SourceRegionTransformTest { + + // Builds a single-operation service model. sdkId drives ServiceNameUtil.getSmithyServiceName. + private static Model modelWithOp(String sdkId, String opName, String reqName) { + StructureShape req = StructureShape.builder() + .id("com.example#" + reqName) + .addMember(MemberShape.builder() + .id("com.example#" + reqName + "$ExistingMember") + .target("smithy.api#String") + .build()) + .build(); + StructureShape out = StructureShape.builder().id("com.example#" + opName + "Result").build(); + OperationShape op = OperationShape.builder() + .id("com.example#" + opName) + .input(req.getId()) + .output(out.getId()) + .build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService") + .version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace(sdkId.toLowerCase()) + .cloudFormationName(sdkId).cloudTrailEventSource(sdkId.toLowerCase()).build()) + .addOperation(op.getId()) + .build(); + return Model.assembler().addShapes(req, out, op, service).assemble().unwrap(); + } + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + @Test + void injectsSourceRegionIntoRdsRequest() { + Model m = modelWithOp("RDS", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotRequest"); + Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + + StructureShape req = out.expectShape( + ShapeId.from("com.example#CopyDBClusterSnapshotRequest"), StructureShape.class); + assertTrue(req.getMember("SourceRegion").isPresent()); + assertEquals("smithy.api#String", + req.getMember("SourceRegion").get().getTarget().toString()); + } + + @Test + void noOpForUntargetedOperation() { + // Operation not in the RDS table -> unchanged + Model m = modelWithOp("RDS", "DescribeDBClusters", "DescribeDBClustersRequest"); + Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + assertTrue(out.expectShape(ShapeId.from("com.example#DescribeDBClustersRequest"), + StructureShape.class).getMember("SourceRegion").isEmpty()); + } + + @Test + void noOpForUntargetedService() { + Model m = modelWithOp("SomeOther", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotRequest"); + Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + assertSame(m, out); + } + + @Test + void idempotent_doesNotDuplicateExistingMember() { + Model m = modelWithOp("RDS", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotRequest"); + Model once = SourceRegionTransform.asTransform().apply(m, service(m)); + Model twice = SourceRegionTransform.asTransform().apply(once, service(once)); + long count = twice.expectShape(ShapeId.from("com.example#CopyDBClusterSnapshotRequest"), + StructureShape.class).members().stream() + .filter(mem -> mem.getMemberName().equals("SourceRegion")).count(); + assertEquals(1, count); + } + + @Test + void injectsSourceRegionIntoDocDbRequest() { + Model m = modelWithOp("DocDB", "CreateDBCluster", "CreateDBClusterMessage"); + Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + assertTrue(out.expectShape(ShapeId.from("com.example#CreateDBClusterMessage"), + StructureShape.class).getMember("SourceRegion").isPresent()); + } + + @Test + void injectsSourceRegionIntoNeptuneRequest() { + Model m = modelWithOp("Neptune", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotMessage"); + Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + assertTrue(out.expectShape(ShapeId.from("com.example#CopyDBClusterSnapshotMessage"), + StructureShape.class).getMember("SourceRegion").isPresent()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java new file mode 100644 index 00000000000..849302eebaa --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java @@ -0,0 +1,91 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.model.EnumRenderer; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class SqsTransformsTest { + + private static final List ADDED = List.of( + "SentTimestamp", "ApproximateFirstReceiveTimestamp", "ApproximateReceiveCount", "SenderId"); + + private static ServiceShape sqsService() { + return ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId("SQS").arnNamespace("sqs") + .cloudFormationName("SQS").cloudTrailEventSource("sqs").build()) + .build(); + } + + @Test + void addsValuesToEnumShape() { + EnumShape enumShape = EnumShape.builder() + .id("com.example#QueueAttributeName") + .addMember("All", "All") + .addMember("Policy", "Policy") + .build(); + Model m = Model.assembler().addShapes(enumShape, sqsService()).assemble().unwrap(); + ServiceShape svc = m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + + Model out = SqsTransforms.asTransform().apply(m, svc); + List values = EnumRenderer.getEnumValues( + out.expectShape(ShapeId.from("com.example#QueueAttributeName"))); + assertTrue(values.containsAll(ADDED)); + assertTrue(values.contains("All")); // originals preserved + } + + @Test + void addsValuesToStringEnumTrait() { + StringShape s = StringShape.builder() + .id("com.example#QueueAttributeName") + .addTrait(EnumTrait.builder() + .addEnum(EnumDefinition.builder().value("All").build()) + .addEnum(EnumDefinition.builder().value("Policy").build()) + .build()) + .build(); + Model m = Model.assembler().addShapes(s, sqsService()).assemble().unwrap(); + ServiceShape svc = m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + + Model out = SqsTransforms.asTransform().apply(m, svc); + List values = EnumRenderer.getEnumValues( + out.expectShape(ShapeId.from("com.example#QueueAttributeName"))); + assertTrue(values.containsAll(ADDED)); + } + + @Test + void appendValuesRejectsNonIdentifierValue() { + EnumShape enumShape = EnumShape.builder() + .id("com.example#QueueAttributeName").addMember("All", "All").build(); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> TransformSupport.appendValues(enumShape, List.of("bad-value"))); + assertTrue(ex.getMessage().contains("bad-value")); + assertTrue(ex.getMessage().contains("com.example#QueueAttributeName")); + } + + @Test + void idempotent() { + EnumShape enumShape = EnumShape.builder() + .id("com.example#QueueAttributeName").addMember("All", "All").build(); + Model m = Model.assembler().addShapes(enumShape, sqsService()).assemble().unwrap(); + ServiceShape svc = m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + + Model once = SqsTransforms.asTransform().apply(m, svc); + Model twice = SqsTransforms.asTransform().apply(once, svc); + long senderId = EnumRenderer.getEnumValues( + twice.expectShape(ShapeId.from("com.example#QueueAttributeName"))) + .stream().filter("SenderId"::equals).count(); + assertEquals(1, senderId); + } +} From e61fa21e71c7f01034d124979b64291693bcf676 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 19 Aug 2026 14:15:42 -0400 Subject: [PATCH 02/44] Smithy: recursively include nested container element types in CppTypeMapper Descend through nested list/map shapes so leaf struct/enum headers reach the generated header even through nested containers (e.g. apigateway Deployment.apiSummary: Map>), matching C2J's recursive unwrap. Recursion is bounded by container-nesting depth, and @sparse still pulls in at each nested container level. --- .../generators/model/CppTypeMapper.java | 55 ++++-- .../generators/model/CppTypeMapperTest.java | 160 ++++++++++++++++++ 2 files changed, 197 insertions(+), 18 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java index 12d7e3ce8cd..d215e88d875 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java @@ -275,24 +275,10 @@ public static List getIncludesForShape(Shape structureShape, Model model } } else { addMemberInclude(includes, target, selfId, model, projectName); - // For list/map, also include the element/key/value types - if (target.isListShape()) { - ListShape list = target.asListShape().get(); - addMemberInclude(includes, model.expectShape(list.getMember().getTarget()), - selfId, model, projectName); - } - if (target.isMapShape()) { - MapShape map = target.asMapShape().get(); - addMemberInclude(includes, model.expectShape(map.getKey().getTarget()), - selfId, model, projectName); - addMemberInclude(includes, model.expectShape(map.getValue().getTarget()), - selfId, model, projectName); - } - // A @sparse list/map wraps its element/value in Aws::Crt::Optional, declared in - // . Matches C2J's generated SparseNullsOperationRequest.h. - if ((target.isListShape() || target.isMapShape()) && target.hasTrait(SparseTrait.class)) { - includes.add(""); - } + // For list/map, recursively include every nested element/key/value type so leaf + // struct/enum headers reach the surface even through nested containers (e.g. + // apigateway Deployment.apiSummary: Map>). + addContainerIncludes(includes, target, selfId, model, projectName); } // @idempotencyToken members are brace-initialized with // Aws::Utils::UUID::PseudoRandomUUID(), which requires UUID.h. Matches C2J @@ -316,6 +302,39 @@ private static void addMemberInclude(Set includes, Shape shape, ShapeId } } + /** + * Recursively adds member-type includes for every nested element/key/value of a list or map + * shape. Recursion only descends through further list/map shapes and stops at + * structures/enums/scalars, so it is bounded by the container-nesting depth (no infinite + * recursion). {@code addMemberInclude} remains a no-op for container/scalar shapes without + * their own header. This lets a member typed, e.g., {@code Map>} + * reach {@code Leaf}'s header, matching C2J's recursive unwrap. + * + *

The {@code @sparse}->{@code } handling fires at each nested + * container level that is sparse, matching C2J's generated headers. + */ + private static void addContainerIncludes(Set includes, Shape target, ShapeId selfId, + Model model, String projectName) { + if (target.isListShape()) { + Shape elem = model.expectShape(target.asListShape().get().getMember().getTarget()); + addMemberInclude(includes, elem, selfId, model, projectName); + addContainerIncludes(includes, elem, selfId, model, projectName); + } else if (target.isMapShape()) { + MapShape map = target.asMapShape().get(); + Shape key = model.expectShape(map.getKey().getTarget()); + Shape value = model.expectShape(map.getValue().getTarget()); + addMemberInclude(includes, key, selfId, model, projectName); + addMemberInclude(includes, value, selfId, model, projectName); + addContainerIncludes(includes, key, selfId, model, projectName); + addContainerIncludes(includes, value, selfId, model, projectName); + } + // A @sparse list/map wraps its element/value in Aws::Crt::Optional, declared in + // . Matches C2J's generated SparseNullsOperationRequest.h. + if ((target.isListShape() || target.isMapShape()) && target.hasTrait(SparseTrait.class)) { + includes.add(""); + } + } + /** * Returns the sorted C++ class names of every direct member whose target forms a reference * cycle with {@code structureShape} (see {@link #isRecursiveStructMember}). These are stored diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java index d5e1754668d..d29a3b6a107 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java @@ -641,4 +641,164 @@ void getIncludesForShape_withMapMember_includesMapKeyAndValue() { assertTrue(includes.contains("")); assertTrue(includes.contains("")); } + + @Test + void getIncludesForShape_withNestedMapOfMap_includesLeafStructHeader() { + // apigateway Deployment.apiSummary is Map>. The outer + // map's value is itself a map (no header of its own), so a one-level unwrap stops before + // reaching the leaf struct MethodSnapshot and its header is dropped — an incomplete-type + // compile error. C2J recursively unwraps nested containers to include all leaf headers. + StringShape str = StringShape.builder().id("com.example#Str").build(); + StructureShape leaf = StructureShape.builder().id("com.example#MethodSnapshot").build(); + MapShape innerMap = MapShape.builder() + .id("com.example#MapOfMethodSnapshot") + .key(MemberShape.builder().id("com.example#MapOfMethodSnapshot$key").target("com.example#Str").build()) + .value(MemberShape.builder().id("com.example#MapOfMethodSnapshot$value") + .target("com.example#MethodSnapshot").build()) + .build(); + MapShape outerMap = MapShape.builder() + .id("com.example#PathToMapOfMethodSnapshot") + .key(MemberShape.builder().id("com.example#PathToMapOfMethodSnapshot$key").target("com.example#Str").build()) + .value(MemberShape.builder().id("com.example#PathToMapOfMethodSnapshot$value") + .target("com.example#MapOfMethodSnapshot").build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("com.example#Deployment") + .addMember("apiSummary", outerMap.getId()) + .build(); + Model model = Model.builder().addShapes(str, leaf, innerMap, outerMap, struct).build(); + + List includes = CppTypeMapper.getIncludesForShape(struct, model, "apigateway"); + assertTrue(includes.contains(""), includes.toString()); + assertTrue(includes.contains(""), includes.toString()); + assertTrue(includes.contains(""), + "nested map-of-map must include leaf struct header: " + includes); + } + + @Test + void getIncludesForShape_withListOfMap_includesLeafStructHeader() { + // List>: the list element is a map (no header), so the leaf struct + // header lives two container levels deep and requires recursive unwrapping. + StringShape str = StringShape.builder().id("com.example#Str").build(); + StructureShape leaf = StructureShape.builder().id("com.example#Item").build(); + MapShape map = MapShape.builder() + .id("com.example#ItemMap") + .key(MemberShape.builder().id("com.example#ItemMap$key").target("com.example#Str").build()) + .value(MemberShape.builder().id("com.example#ItemMap$value").target("com.example#Item").build()) + .build(); + ListShape list = ListShape.builder() + .id("com.example#ListOfItemMap") + .member(MemberShape.builder().id("com.example#ListOfItemMap$member").target("com.example#ItemMap").build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("com.example#MyRequest") + .addMember("rows", list.getId()) + .build(); + Model model = Model.builder().addShapes(str, leaf, map, list, struct).build(); + + List includes = CppTypeMapper.getIncludesForShape(struct, model, "myservice"); + assertTrue(includes.contains(""), includes.toString()); + assertTrue(includes.contains(""), includes.toString()); + assertTrue(includes.contains(""), + "list-of-map must include leaf struct header: " + includes); + } + + @Test + void getIncludesForShape_withMapOfList_includesLeafStructHeader() { + // Map>: the map value is a list (no header), so recursive unwrap + // is needed to reach the leaf struct header. + StringShape str = StringShape.builder().id("com.example#Str").build(); + StructureShape leaf = StructureShape.builder().id("com.example#Item").build(); + ListShape list = ListShape.builder() + .id("com.example#ItemList") + .member(MemberShape.builder().id("com.example#ItemList$member").target("com.example#Item").build()) + .build(); + MapShape map = MapShape.builder() + .id("com.example#MapOfItemList") + .key(MemberShape.builder().id("com.example#MapOfItemList$key").target("com.example#Str").build()) + .value(MemberShape.builder().id("com.example#MapOfItemList$value").target("com.example#ItemList").build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("com.example#MyRequest") + .addMember("grouped", map.getId()) + .build(); + Model model = Model.builder().addShapes(str, leaf, list, map, struct).build(); + + List includes = CppTypeMapper.getIncludesForShape(struct, model, "myservice"); + assertTrue(includes.contains(""), includes.toString()); + assertTrue(includes.contains(""), includes.toString()); + assertTrue(includes.contains(""), + "map-of-list must include leaf struct header: " + includes); + } + + @Test + void getIncludesForShape_withNestedSparseMap_includesOptionalHeader() { + // An inner @sparse map nested inside an outer map must still contribute ; + // the sparse handling fires at each nested container level. + StringShape str = StringShape.builder().id("com.example#Str").build(); + MapShape innerMap = MapShape.builder() + .id("com.example#SparseInnerMap") + .key(MemberShape.builder().id("com.example#SparseInnerMap$key").target("com.example#Str").build()) + .value(MemberShape.builder().id("com.example#SparseInnerMap$value").target("com.example#Str").build()) + .addTrait(new software.amazon.smithy.model.traits.SparseTrait()) + .build(); + MapShape outerMap = MapShape.builder() + .id("com.example#OuterMap") + .key(MemberShape.builder().id("com.example#OuterMap$key").target("com.example#Str").build()) + .value(MemberShape.builder().id("com.example#OuterMap$value").target("com.example#SparseInnerMap").build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("com.example#MyRequest") + .addMember("data", outerMap.getId()) + .build(); + Model model = Model.builder().addShapes(str, innerMap, outerMap, struct).build(); + + List includes = CppTypeMapper.getIncludesForShape(struct, model, "myservice"); + assertTrue(includes.contains(""), + "nested sparse map must include Optional.h: " + includes); + } + + @Test + void getIncludesForShape_singleLevelMapOfStruct_unchanged() { + // Regression guard: a single-level Map must include exactly the same + // headers after the recursive-unwrap change as before it. + StringShape str = StringShape.builder().id("com.example#Str").build(); + StructureShape leaf = StructureShape.builder().id("com.example#Item").build(); + MapShape map = MapShape.builder() + .id("com.example#ItemMap") + .key(MemberShape.builder().id("com.example#ItemMap$key").target("com.example#Str").build()) + .value(MemberShape.builder().id("com.example#ItemMap$value").target("com.example#Item").build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("com.example#MyRequest") + .addMember("data", map.getId()) + .build(); + Model model = Model.builder().addShapes(str, leaf, map, struct).build(); + + List includes = CppTypeMapper.getIncludesForShape(struct, model, "myservice"); + assertEquals(List.of( + "", + "", + ""), includes); + } + + @Test + void getIncludesForShape_singleLevelListOfStruct_unchanged() { + // Regression guard: a single-level List is unchanged by the recursive-unwrap. + StructureShape leaf = StructureShape.builder().id("com.example#Item").build(); + ListShape list = ListShape.builder() + .id("com.example#ItemList") + .member(MemberShape.builder().id("com.example#ItemList$member").target("com.example#Item").build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("com.example#MyRequest") + .addMember("items", list.getId()) + .build(); + Model model = Model.builder().addShapes(leaf, list, struct).build(); + + List includes = CppTypeMapper.getIncludesForShape(struct, model, "myservice"); + assertEquals(List.of( + "", + ""), includes); + } } From f0c62c14e58b1f42327b1bb7a1228ee81a10d440 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 19 Aug 2026 14:15:43 -0400 Subject: [PATCH 03/44] Smithy: guard Windows-macro-colliding enum constants in EnumRenderer Emit #if defined(_WIN32) && defined(X) / #undef X guards for enum constants that collide with a Windows preprocessor macro (DynamoDB IN, EC2 interface, S3Crt IGNORE), mirroring C2J PlatformAndKeywordSanitizer.PREDEFINED_SYMBOLS_MAPPING and ModelEnumHeader.vm. --- .../generators/model/EnumRenderer.java | 43 ++++++++++++++++ .../generators/model/EnumRendererTest.java | 51 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java index 4901d639a66..043fb5585ab 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -50,6 +51,20 @@ public static void renderHeader(CppWriter writer, Shape enumShape, String servic writer.write("#include ", projectName, serviceName); writer.write(""); + + // Windows defines some enum values as preprocessor macros (e.g. EC2's `interface` via + // ). Undefine them so the generated enum constant compiles, matching C2J's + // ModelEnumHeader.vm predefined-symbol guard. + List windowsMacros = predefinedWindowsSymbols(serviceName, values); + if (!windowsMacros.isEmpty()) { + for (String macro : windowsMacros) { + writer.write("#if defined(_WIN32) && defined($L)", macro); + writer.write("#undef $L", macro); + writer.write("#endif"); + } + writer.write(""); + } + writer.write("namespace Aws {"); writer.write("namespace $L {", serviceName); writer.write("namespace Model {"); @@ -225,6 +240,34 @@ private static List getEnumWireValues(Shape enumShape) { "STATIC", "T_CHAR", "DOMAIN", "OVERFLOW", "WINDOWS" ); + /** + * Per-service enum constant names that collide with a Windows preprocessor macro and must be + * {@code #undef}'d in the enum header. Keyed by C++ service namespace, mirroring C2J + * PlatformAndKeywordSanitizer.PREDEFINED_SYMBOLS_MAPPING. + */ + private static final Map> PREDEFINED_WINDOWS_SYMBOLS = Map.of( + "DynamoDB", Set.of("IN"), + "EC2", Set.of("interface"), + "S3Crt", Set.of("IGNORE") + ); + + /** + * Returns, in enum-declaration order, the sanitized enum constant names of {@code values} that + * collide with a Windows macro for {@code serviceNamespace} (see + * {@link #PREDEFINED_WINDOWS_SYMBOLS}). Empty when the service has no such symbols. + * + * @param serviceNamespace the C++ service namespace (e.g., "EC2") + * @param values the sanitized enum constant names in declaration order + * @return the subset needing a {@code #undef} guard, preserving declaration order + */ + static List predefinedWindowsSymbols(String serviceNamespace, List values) { + Set symbols = PREDEFINED_WINDOWS_SYMBOLS.get(serviceNamespace); + if (symbols == null) { + return List.of(); + } + return values.stream().filter(symbols::contains).collect(Collectors.toList()); + } + /** * Sanitizes an enum wire value into a valid C++ identifier, matching C2J * PlatformAndKeywordSanitizer.fixEnumValue() behavior. diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java index 35dd58fd79a..f22a5581945 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java @@ -327,4 +327,55 @@ void sanitizeEnumValue_handlesCorrectedForbiddenWords() { // Verify module (was misspelled as moduel) is now correctly forbidden assertEquals("module_", EnumRenderer.sanitizeEnumValue("module")); } + + @Test + void renderHeader_emitsWindowsUndefGuardForEc2InterfaceEnumValue() { + // EC2's NetworkInterfaceType has an `interface` value that collides with the Windows + // `interface` macro (); C2J's ModelEnumHeader.vm #undef's it. + EnumShape enumShape = EnumShape.builder() + .id("com.example#NetworkInterfaceType") + .addMember("interface", "interface") + .addMember("natGateway", "natGateway") + .build(); + CppWriter writer = new CppWriter(); + EnumRenderer.renderHeader(writer, enumShape, "EC2", "AWS_EC2_API", "ec2"); + String output = writer.toString(); + assertTrue(output.contains("#if defined(_WIN32) && defined(interface)"), + "Missing Windows guard for `interface`: " + output); + assertTrue(output.contains("#undef interface"), "Missing #undef interface: " + output); + assertTrue(output.contains("#endif"), "Missing #endif: " + output); + // The guard must sit before the namespace block, matching C2J. + assertTrue(output.indexOf("#undef interface") < output.indexOf("namespace Aws {"), + "#undef must precede the namespace block: " + output); + } + + @Test + void renderHeader_noWindowsUndefGuardForOtherServicesWithInterfaceValue() { + // The mapping is per-service: only EC2 gets the `interface` guard. A different service + // with the same enum value must not emit it (matches C2J's namespace-keyed mapping). + EnumShape enumShape = EnumShape.builder() + .id("com.example#SomeType") + .addMember("interface", "interface") + .build(); + CppWriter writer = new CppWriter(); + EnumRenderer.renderHeader(writer, enumShape, "TestService", "AWS_TESTSERVICE_API", "testservice"); + String output = writer.toString(); + assertFalse(output.contains("#undef interface"), + "Only EC2 should get the interface guard: " + output); + } + + @Test + void predefinedWindowsSymbols_matchesC2jNamespaceKeyedMapping() { + // EC2 -> interface, DynamoDB -> IN, S3Crt -> IGNORE; order follows the value list. + assertEquals(List.of("interface"), + EnumRenderer.predefinedWindowsSymbols("EC2", List.of("natGateway", "interface", "efa"))); + assertEquals(List.of("IN"), + EnumRenderer.predefinedWindowsSymbols("DynamoDB", List.of("EQ", "IN", "LE"))); + assertEquals(List.of("IGNORE"), + EnumRenderer.predefinedWindowsSymbols("S3Crt", List.of("IGNORE"))); + // No collisions -> empty. + assertTrue(EnumRenderer.predefinedWindowsSymbols("EC2", List.of("natGateway")).isEmpty()); + // Unknown service -> empty even if a value matches another service's symbol. + assertTrue(EnumRenderer.predefinedWindowsSymbols("TestService", List.of("interface")).isEmpty()); + } } From 7a486482cba6c1db924b54be71b3a77ce7e27f1d Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 19 Aug 2026 14:15:55 -0400 Subject: [PATCH 04/44] Smithy: honor per-service result-class suffix in ResultRenderer Name result classes and files via ShapeUtil.getResultSuffix instead of a hardcoded "Result", so services like EC2 emit *Response result classes, matching the legacy C2J generator. --- .../model/renderers/ResultRenderer.java | 9 +++--- .../generators/model/ResultRendererTest.java | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java index dbd565fe9b5..438c2244e3e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java @@ -5,6 +5,7 @@ package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.ShapeUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppNames; import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; @@ -64,7 +65,7 @@ private String streamingPayloadMemberName(StructureShape shape) { private void renderHeader(CppWriterDelegator writerDelegator, StructureShape shape, OperationShape operation) { - String className = operation.getId().getName() + "Result"; + String className = operation.getId().getName() + ShapeUtil.getResultSuffix(ctx.model(), operation, ctx.smithyServiceName()); String fileName = "include/aws/" + ctx.smithyServiceName() + "/model/" + className + ".h"; writerDelegator.useFileWriter(fileName, writer -> { writer.write("#pragma once"); @@ -139,7 +140,7 @@ private void renderHeader(CppWriterDelegator writerDelegator, private void renderSource(CppWriterDelegator writerDelegator, StructureShape shape, OperationShape operation) { - String className = operation.getId().getName() + "Result"; + String className = operation.getId().getName() + ShapeUtil.getResultSuffix(ctx.model(), operation, ctx.smithyServiceName()); String fileName = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(fileName, writer -> { @@ -164,7 +165,7 @@ private void renderSource(CppWriterDelegator writerDelegator, */ private void renderStreamingHeader(CppWriterDelegator writerDelegator, StructureShape shape, OperationShape operation) { - String className = operation.getId().getName() + "Result"; + String className = operation.getId().getName() + ShapeUtil.getResultSuffix(ctx.model(), operation, ctx.smithyServiceName()); String streamMember = streamingPayloadMemberName(shape); String fileName = "include/aws/" + ctx.smithyServiceName() + "/model/" + className + ".h"; writerDelegator.useFileWriter(fileName, writer -> { @@ -257,7 +258,7 @@ private void renderStreamingHeader(CppWriterDelegator writerDelegator, */ private void renderStreamingSource(CppWriterDelegator writerDelegator, StructureShape shape, OperationShape operation) { - String className = operation.getId().getName() + "Result"; + String className = operation.getId().getName() + ShapeUtil.getResultSuffix(ctx.model(), operation, ctx.smithyServiceName()); String streamField = CppNames.fieldName(streamingPayloadMemberName(shape)); String fileName = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(fileName, writer -> { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java index eeea14d4920..cbbe716190b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java @@ -57,6 +57,38 @@ private static String renderResultHeader(Trait protocolTrait) { .filter(p -> p.toString().endsWith("DoThingResult.h")).findFirst().orElseThrow()).orElseThrow(); } + /** Renders the result header for the one-member model under the given smithyServiceName. */ + private static java.util.List renderResultFileNames(Trait protocolTrait, String smithyServiceName) { + Model model = oneMemberOutputModel(protocolTrait); + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + Protocol protocol = ProtocolResolver.resolve(service, model); + new ResultRenderer( + ShapeClassifier.classify(model, service, protocol).results(), + new RenderContext(model, service, ProtocolResolver.traitsFor(protocol), + "Example", "AWS_EXAMPLE_API", smithyServiceName)).render(delegator); + delegator.flushWriters(); + return manifest.getFiles().stream().map(java.nio.file.Path::toString) + .collect(java.util.stream.Collectors.toList()); + } + + @Test + void ec2Result_usesResponseSuffix() { + // ShapeUtil.getResultSuffix returns "Response" for the ec2 service, so EC2 result + // classes/files must be named *Response, matching the legacy C2J generator. + java.util.List ec2Files = + renderResultFileNames(software.amazon.smithy.aws.traits.protocols.RestJson1Trait.builder().build(), "ec2"); + assertTrue(ec2Files.stream().anyMatch(f -> f.endsWith("DoThingResponse.h")), ec2Files.toString()); + assertTrue(ec2Files.stream().anyMatch(f -> f.endsWith("DoThingResponse.cpp")), ec2Files.toString()); + assertFalse(ec2Files.stream().anyMatch(f -> f.endsWith("DoThingResult.h")), ec2Files.toString()); + + // Non-ec2 services keep the "Result" suffix. + java.util.List otherFiles = + renderResultFileNames(software.amazon.smithy.aws.traits.protocols.RestJson1Trait.builder().build(), "example"); + assertTrue(otherFiles.stream().anyMatch(f -> f.endsWith("DoThingResult.h")), otherFiles.toString()); + } + @Test void cborResult_omitsHasBeenSetAccessors() { // C2J's CborResultHeader.vm sets useRequiredField=false, so result classes never emit From 75e457a2bb1a538f12cbbb70e9ad9a80b7803b59 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 19 Aug 2026 14:15:55 -0400 Subject: [PATCH 05/44] Smithy: blob-payload event rendering and dual-role sub-object requestId stamp Classify @streaming-union events whose sole payload is a single @eventPayload blob member as header-only blob-carrier events (C2J eventPayloadType == "blob") and render them via a new EventPayloadRenderer instead of as JSON sub-objects. Stamp the top-level requestId onto dual-role sub-objects (operation outputs also referenced as members) for JSON-family protocols, gated out for Query/EC2 which inject ResponseMetadata instead. MemberRenderer now renders the injected ResponseMetadata envelope as always-present (no HasBeenSet getter, flag true), initializes required-member flags in useRequiredField contexts, and keeps event stream / raw streaming payload flags false, matching C2J. --- .../generators/model/MemberRenderer.java | 80 +++++++++- .../generators/model/ModelGenerator.java | 4 +- .../generators/model/ShapeClassifier.java | 73 ++++++++- .../model/renderers/EventPayloadRenderer.java | 116 ++++++++++++++ .../model/renderers/SubObjectRenderer.java | 57 ++++++- .../model/EventPayloadRendererTest.java | 86 ++++++++++ .../generators/model/MemberRendererTest.java | 72 +++++++++ .../generators/model/ShapeClassifierTest.java | 147 ++++++++++++++++++ .../model/SubObjectRendererTest.java | 80 ++++++++++ 9 files changed, 697 insertions(+), 18 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java index 03221bf1fc6..6894d604f18 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java @@ -7,13 +7,16 @@ import static com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper.isPrimitive; import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.HttpPayloadTrait; import software.amazon.smithy.model.traits.IdempotencyTokenTrait; import software.amazon.smithy.model.traits.SparseTrait; +import software.amazon.smithy.model.traits.StreamingTrait; import java.util.Map; @@ -102,7 +105,12 @@ public void renderPublicAccessors(CppWriter writer) { writer.write("inline const $L& Get$L() const { return $L; }", cppType, methodName, fieldName); } - if (emitHasBeenSet) { + // The framework-injected ResponseMetadata envelope is always present, so — like C2J — + // it gets no HasBeenSet getter (and its flag is initialized true, below). Every other + // member, including modeled @required ones, tracks presence via HasBeenSet, matching + // C2J's mass-clear of required-ness. `emitHasBeenSet` is the useRequiredField context + // (true for sub-objects/requests, false for results). + if (emitHasBeenSet && !isInjectedResponseMetadata(member)) { writer.write("inline bool $LHasBeenSet() const { return $LHasBeenSet; }", methodName, fieldName); } @@ -255,10 +263,25 @@ public void renderPrivateSection(CppWriter writer) { * {@code m_requestId} field and its {@code HasBeenSet} flag in the private section. */ public static void renderRequestIdAccessors(CppWriter writer, String className) { + renderRequestIdAccessors(writer, className, false); + } + + /** + * Renders the top-level {@code RequestId} accessor group. When {@code withHasBeenSetGetter} is + * {@code true}, the {@code inline bool RequestIdHasBeenSet() const} getter is emitted after the + * {@code GetRequestId} getter — the MODEL-class variant C2J stamps onto an operation-output + * shape that is also referenced as a member (dual-role sub-object). Result classes pass + * {@code false} (no {@code HasBeenSet} getter), matching {@link #forResult}. + */ + public static void renderRequestIdAccessors(CppWriter writer, String className, + boolean withHasBeenSetGetter) { writer.write(""); writer.write("///@{"); writer.write(""); writer.write("inline const Aws::String& GetRequestId() const { return m_requestId; }"); + if (withHasBeenSetGetter) { + writer.write("inline bool RequestIdHasBeenSet() const { return m_requestIdHasBeenSet; }"); + } writer.write("template "); writer.openBlock("void SetRequestId(RequestIdT&& value) {", "}", () -> { writer.write("m_requestIdHasBeenSet = true;"); @@ -299,14 +322,57 @@ private static void writeDataMember(CppWriter writer, MemberShape member, String } /** - * Writes a single HasBeenSet flag. {@code @idempotencyToken} members default to {@code true} - * because they are auto-populated at construction; all others default to {@code false}. - * Matches C2J's ModelClassMembersAndInlines.vm. + * Writes a single HasBeenSet flag. Matches C2J's ModelClassMembersAndInlines.vm: the flag is + * initialized to {@code true} for an {@code @idempotencyToken} member (auto-populated at + * construction) or a {@code @required} member in a useRequiredField context ({@code emitHasBeenSet} + * — sub-objects/requests, but not results), except when the member is an event stream or a raw + * streaming payload. All other members default to {@code false}. */ - private static void writeHasBeenSetFlag(CppWriter writer, MemberShape member, String memberName) { + private void writeHasBeenSetFlag(CppWriter writer, MemberShape member, String memberName) { String fieldName = CppNames.fieldName(memberName); - boolean initialValue = member.hasTrait(IdempotencyTokenTrait.class); - writer.write("bool $LHasBeenSet = $L;", fieldName, initialValue); + writer.write("bool $LHasBeenSet = $L;", fieldName, initialHasBeenSet(member)); + } + + /** True if this member's HasBeenSet flag is initialized to {@code true}. Mirrors C2J. */ + private boolean initialHasBeenSet(MemberShape member) { + if (isEventStreamMember(member) || isRawStreamingPayloadMember(member)) { + return false; + } + return member.hasTrait(IdempotencyTokenTrait.class) + || (emitHasBeenSet && isInjectedResponseMetadata(member)); + } + + /** + * True if this member is the framework-injected {@code ResponseMetadata} envelope + * ({@code GlobalTransforms.injectResponseMetadata}): a member named {@code ResponseMetadata} + * whose target is the injected {@code ResponseMetadata} structure. It is the only member + * rendered as always-present (no {@code HasBeenSet} getter; flag initialized true in a + * HasBeenSet context), matching C2J, which likewise identifies ResponseMetadata by name. + * {@code injectResponseMetadata} fails fast on any modeled ResponseMetadata collision, so this + * name-based check is unambiguous. + */ + private boolean isInjectedResponseMetadata(MemberShape member) { + return GlobalTransforms.RESPONSE_METADATA.equals(member.getMemberName()) + && GlobalTransforms.RESPONSE_METADATA.equals( + model.expectShape(member.getTarget()).getId().getName()); + } + + /** True if the member targets a {@code @streaming} union (an event stream member). */ + private boolean isEventStreamMember(MemberShape member) { + Shape target = model.expectShape(member.getTarget()); + return target.isUnionShape() && target.hasTrait(StreamingTrait.class); + } + + /** + * True if the member is a raw streaming {@code @httpPayload} (blob/string, or explicitly + * {@code @streaming}) that is not an event stream. Mirrors {@code ShapeClassifier}'s predicate. + */ + private boolean isRawStreamingPayloadMember(MemberShape member) { + if (!member.hasTrait(HttpPayloadTrait.class) || StreamingTrait.isEventStream(model, member)) { + return false; + } + Shape target = model.expectShape(member.getTarget()); + return target.isBlobShape() || target.isStringShape() || target.hasTrait(StreamingTrait.class); } /** diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java index f8e4eb7f84f..1b07839704d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java @@ -8,6 +8,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.ClassifiedShapes; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EnumShapeRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EventPayloadRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EventStreamRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.OutgoingEventStreamRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestRenderer; @@ -64,11 +65,12 @@ public void generateAll() { private List buildRenderers(ClassifiedShapes classified, RenderContext ctx) { List renderers = new ArrayList<>(); renderers.add(new EnumShapeRenderer(classified.enums(), ctx)); - renderers.add(new SubObjectRenderer(classified.subObjects(), ctx)); + renderers.add(new SubObjectRenderer(classified.subObjects(), classified.resultOutputIds(), ctx)); renderers.add(new RequestRenderer(classified.requests(), ctx)); renderers.add(new ResultRenderer(classified.results(), ctx)); renderers.add(new EventStreamRenderer(classified.eventStreamHandlers(), ctx)); renderers.add(new OutgoingEventStreamRenderer(classified.outgoingEventStreams(), ctx)); + renderers.add(new EventPayloadRenderer(classified.blobPayloadEvents(), ctx)); return renderers; } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java index 5d57f27da33..0dc0eaf46ad 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java @@ -18,6 +18,7 @@ import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.model.traits.EventPayloadTrait; import software.amazon.smithy.model.traits.HttpPayloadTrait; import software.amazon.smithy.model.traits.StreamingTrait; @@ -72,6 +73,13 @@ public record EventStreamInfo(String operationName, StructureShape requestShape, * @param enums EnumShape or StringShape with @enum trait * @param eventStreamHandlers operation + request/result shape tuples for event stream handlers * @param outgoingEventStreams outgoing event stream shapes (header only) + * @param blobPayloadEvents event structs (members of a {@code @streaming} union) whose sole + * payload is a single {@code @eventPayload} blob member; rendered + * header-only as a blob-carrier event (C2J {@code eventPayloadType == + * "blob"}), never as a JSON sub-object + * @param resultOutputIds shape ids of every operation output; a sub-object whose id is in + * this set is "dual-role" (an output that is also a member) and, for + * JSON-family protocols, receives the C2J {@code requestId} stamp */ public record ClassifiedShapes( List requests, @@ -79,7 +87,9 @@ public record ClassifiedShapes( List subObjects, List enums, List eventStreamHandlers, - List outgoingEventStreams + List outgoingEventStreams, + List blobPayloadEvents, + Set resultOutputIds ) {} private ShapeClassifier() {} @@ -109,6 +119,7 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto List enums = new ArrayList<>(); List eventStreamHandlers = new ArrayList<>(); List outgoingEventStreams = new ArrayList<>(); + List blobPayloadEvents = new ArrayList<>(); // Collect operation inputs/outputs and identify event stream handlers for (OperationShape op : index.getContainedOperations(service)) { @@ -151,15 +162,38 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto }); } + // Shape ids referenced as a member by any reachable shape (includes list/map element targets). + Set memberTargetIds = new HashSet<>(); + for (ShapeId id : reachable) { + model.expectShape(id).members().forEach(m -> memberTargetIds.add(m.getTarget())); + } + + // Structs that are members of a reachable @streaming union — i.e. events. A blob-payload + // event is only recognised among these (analogous to memberTargetIds but restricted to + // event unions), so a plain data struct that merely happens to carry an @eventPayload blob + // is never mis-claimed. + Set eventStructIds = new HashSet<>(); + for (ShapeId id : reachable) { + Shape shape = model.expectShape(id); + if (shape.isUnionShape() && shape.hasTrait(StreamingTrait.class)) { + shape.members().forEach(m -> eventStructIds.add(m.getTarget())); + } + } + // Walk all reachable shapes and classify remaining ones for (ShapeId id : reachable) { Shape shape = model.expectShape(id); - if (inputShapeIds.contains(id) || outputShapeIds.contains(id)) { - // Already classified as request/result above + if ((inputShapeIds.contains(id) || outputShapeIds.contains(id)) && !memberTargetIds.contains(id)) { + // classified as request/result and not referenced as a member — nothing more to emit } else if (shape.isEnumShape() || (shape.isStringShape() && shape.hasTrait(EnumTrait.class))) { enums.add(shape); } else if (outgoingEventStreamIds.contains(id)) { // Already collected as an outgoing event stream; do not also render as a data union. + } else if (isBlobPayloadEvent(shape, model, eventStructIds)) { + // A @streaming-union event whose payload is a single @eventPayload blob member is a + // header-only blob-carrier event (C2J eventPayloadType == "blob"), not a JSON + // sub-object. Routed here before the generic structure branch below. + blobPayloadEvents.add(shape); } else if (shape.hasTrait(ErrorTrait.class)) { if (isModeledException(shape.asStructureShape().get(), protocol)) { subObjects.add(shape); @@ -170,7 +204,7 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto } return new ClassifiedShapes(requests, results, subObjects, enums, - eventStreamHandlers, outgoingEventStreams); + eventStreamHandlers, outgoingEventStreams, blobPayloadEvents, outputShapeIds); } /** @@ -212,6 +246,37 @@ private static boolean hasRawStreamingPayload(StructureShape shape, Model model) return false; } + /** + * True if {@code shape} is a blob-payload event: a structure that is a member of a reachable + * {@code @streaming} union (i.e. an event) and carries a member whose trait set includes + * {@code smithy.api#eventPayload} and whose target is a blob. This mirrors C2J's + * {@code eventPayloadType == "blob"} case (C2jModelToGeneratorModelTransformer): a blob member + * is a raw blob payload only when explicitly {@code @eventPayload}. Non-blob eventPayload + * events (e.g. a CompleteEvent with only string members) are NOT claimed — they remain + * sub-objects. + */ + private static boolean isBlobPayloadEvent(Shape shape, Model model, Set eventStructIds) { + if (!shape.isStructureShape() || !eventStructIds.contains(shape.getId())) { + return false; + } + return blobPayloadMemberName(shape.asStructureShape().get(), model).isPresent(); + } + + /** + * Returns the member name of the single {@code @eventPayload} blob member of {@code shape}, or + * empty if the shape has no such member. Used by both the classifier predicate and the + * blob-payload event renderer so they agree on which member becomes the blob payload. + */ + public static Optional blobPayloadMemberName(StructureShape shape, Model model) { + for (MemberShape member : shape.getAllMembers().values()) { + if (member.hasTrait(EventPayloadTrait.class) + && model.expectShape(member.getTarget()).isBlobShape()) { + return Optional.of(member.getMemberName()); + } + } + return Optional.empty(); + } + /** * Returns true if the structure has a member targeting a union with the @streaming trait * (i.e., an event stream member). diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java new file mode 100644 index 00000000000..820f6644f81 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java @@ -0,0 +1,116 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppNames; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.DocumentationTrait; + +import java.util.List; + +/** + * Renders header-only blob-carrier events: an event struct (member of a {@code @streaming} union) + * whose sole payload is a single {@code @eventPayload} blob member (C2J {@code eventPayloadType == + * "blob"}). C2J renders these via {@code EventHeader.vm} as a plain value type carrying an + * {@code Aws::Vector} payload — a bytes constructor, non-template const-ref / rvalue + * accessors, and a {@code GetWithOwnership()} move-out — with NO {@code Jsonize} / + * {@code JsonView} serde and NO {@code .cpp}. + * + *

These shapes are routed here by {@link ShapeClassifier} instead of {@code subObjects}, so the + * generic {@code SubObjectRenderer} JSON path never sees them. + */ +public final class EventPayloadRenderer implements ShapeRenderer { + + private final List blobPayloadEvents; + private final RenderContext ctx; + + public EventPayloadRenderer(List blobPayloadEvents, RenderContext ctx) { + this.blobPayloadEvents = blobPayloadEvents; + this.ctx = ctx; + } + + @Override + public void render(CppWriterDelegator writerDelegator) { + for (Shape shape : blobPayloadEvents) { + shape.asStructureShape().ifPresent(s -> renderHeader(writerDelegator, s)); + } + } + + private void renderHeader(CppWriterDelegator writerDelegator, StructureShape shape) { + String className = CppTypeMapper.cppShapeName(shape); + String memberName = ShapeClassifier.blobPayloadMemberName(shape, ctx.model()) + .orElseThrow(() -> new IllegalStateException( + "Blob-payload event " + shape.getId() + " has no @eventPayload blob member")); + MemberShape payload = shape.getAllMembers().get(memberName); + String methodName = CppNames.capitalize(memberName); + String fieldName = CppNames.fieldName(memberName); + String fileName = "include/aws/" + ctx.smithyServiceName() + "/model/" + className + ".h"; + + writerDelegator.useFileWriter(fileName, writer -> { + writer.write("#pragma once"); + java.util.List includes = new java.util.ArrayList<>(); + includes.add("aws/core/utils/Array.h"); + includes.add("aws/" + ctx.smithyServiceName() + "/" + ctx.namespace() + "_EXPORTS.h"); + IncludeSets.emitAngleIncludes(writer, includes); + writer.write(""); + writer.write("#include "); + writer.write(""); + + ModelFile.modelNamespace(writer, ctx.namespace(), () -> { + MemberRenderer.renderClassDocComment(writer, shape, ctx.smithyServiceName(), ctx.service().getVersion()); + writer.openBlock("class $L {", "};", className, () -> { + writer.write("public:"); + writer.write("$L $L() = default;", ctx.exportMacro(), className); + writer.write("$L $L(Aws::Vector&& value) { $L = std::move(value); }", + ctx.exportMacro(), className, fieldName); + writer.write(""); + writer.write("///@{"); + writeMemberDoc(writer, payload); + writer.write("inline const Aws::Vector& Get$L() const { return $L; }", + methodName, fieldName); + writer.write("inline Aws::Vector&& Get$LWithOwnership() { return std::move($L); }", + methodName, fieldName); + writer.write("inline void Set$L(const Aws::Vector& value) { $LHasBeenSet = true; $L = value; }", + methodName, fieldName, fieldName); + writer.write("inline void Set$L(Aws::Vector&& value) { $LHasBeenSet = true; $L = std::move(value); }", + methodName, fieldName, fieldName); + writer.write("inline $L& With$L(const Aws::Vector& value) { Set$L(value); return *this;}", + className, methodName, methodName); + writer.write("inline $L& With$L(Aws::Vector&& value) { Set$L(std::move(value)); return *this;}", + className, methodName, methodName); + writer.write("///@}"); + writer.write(""); + writer.dedent(); + writer.write("private:"); + writer.indent(); + writer.write("Aws::Vector $L;", fieldName); + writer.write("bool $LHasBeenSet = false;", fieldName); + }); + writer.write(""); + }); + }); + } + + /** + * Emits the payload member's doc comment. Matches C2J's {@code EventHeader.vm}, which always + * renders a {@code /** ... *}{@code /} block from the member's {@code @documentation} + * (whitespace-collapsed). + */ + private void writeMemberDoc(CppWriter writer, MemberShape payload) { + String doc = payload.getTrait(DocumentationTrait.class) + .map(t -> MemberRenderer.collapseWhitespace(t.getValue())) + .orElse(""); + MemberRenderer.writeDocComment(writer, doc); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java index 837c62e72f2..7a6dfdbf970 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java @@ -12,9 +12,11 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.StreamingTrait; import java.util.List; +import java.util.Set; /** * Renders C++ headers and sources for sub-object (intermediate structure) shapes. @@ -22,13 +24,20 @@ public final class SubObjectRenderer implements ShapeRenderer { private final List subObjects; + private final Set resultOutputIds; private final RenderContext ctx; - public SubObjectRenderer(List subObjects, RenderContext ctx) { + public SubObjectRenderer(List subObjects, Set resultOutputIds, RenderContext ctx) { this.subObjects = subObjects; + this.resultOutputIds = resultOutputIds; this.ctx = ctx; } + /** Convenience overload for callers with no dual-role output shapes (e.g. characterization tests). */ + public SubObjectRenderer(List subObjects, RenderContext ctx) { + this(subObjects, java.util.Collections.emptySet(), ctx); + } + @Override public void render(CppWriterDelegator writerDelegator) { for (Shape shape : subObjects) { @@ -48,12 +57,22 @@ public void render(CppWriterDelegator writerDelegator) { private void renderHeader(CppWriterDelegator writerDelegator, Shape shape) { String className = CppTypeMapper.cppShapeName(shape); String fileName = "include/aws/" + ctx.smithyServiceName() + "/model/" + className + ".h"; + // A shape that is BOTH an operation output AND referenced as a member ("dual-role") is + // stamped with the top-level requestId by C2J — but only for JSON-family protocols. Query/EC2 + // instead inject a ResponseMetadata member (GlobalTransforms.injectResponseMetadata), so they + // are gated out here via resultHasTopLevelRequestId(). + boolean stampRequestId = resultOutputIds.contains(shape.getId()) + && ctx.protocolTraits().resultHasTopLevelRequestId(); writerDelegator.useFileWriter(fileName, writer -> { writer.write("#pragma once"); // Includes List includes = new java.util.ArrayList<>(); includes.add("aws/" + ctx.smithyServiceName() + "/" + ctx.namespace() + "_EXPORTS.h"); + if (stampRequestId) { + // The stamped m_requestId is an Aws::String; mirror the result-header include hygiene. + includes.add("aws/core/utils/memory/stl/AWSString.h"); + } for (String memberInc : CppTypeMapper.getIncludesForShape(shape, ctx.model(), ctx.smithyServiceName())) { includes.add(memberInc); } @@ -86,16 +105,42 @@ private void renderHeader(CppWriterDelegator writerDelegator, Shape shape) { ctx.protocolTraits().writeSerdeMethodDecls(writer, ctx.exportMacro(), className, null); // A memberless shape ends right after its serde decls: C2J emits no accessors // and no private: section (ModelClassMembersAndInlines.vm gates both on - // $shape.members.size() > 0). - if (!shape.getAllMembers().isEmpty()) { + // $shape.members.size() > 0) — unless it is a dual-role output, in which case the + // stamped requestId group still needs a private: section. + boolean hasMembers = !shape.getAllMembers().isEmpty(); + if (hasMembers || stampRequestId) { MemberRenderer members = MemberRenderer.forStructure(ctx.model(), shape, className) .wideIntegers(ctx.protocolTraits().widensIntegers()); - writer.write(""); - members.renderPublicAccessors(writer); + if (hasMembers) { + writer.write(""); + members.renderPublicAccessors(writer); + } + if (stampRequestId) { + // MODEL-class requestId group (includes the RequestIdHasBeenSet() getter), + // emitted after the modeled-member accessors. The helper writes its own + // leading blank-line separator. + MemberRenderer.renderRequestIdAccessors(writer, className, true); + } writer.dedent(); writer.write("private:"); writer.indent(); - members.renderPrivateSection(writer); + if (hasMembers) { + members.renderDataMembers(writer); + } + if (stampRequestId) { + // m_requestId trails the modeled data members (blank-line separated, matching + // MemberRenderer's data-member spacing); its flag trails the modeled flags. + if (hasMembers) { + writer.write(""); + } + writer.write("Aws::String m_requestId;"); + } + if (hasMembers) { + members.renderHasBeenSetFlags(writer); + } + if (stampRequestId) { + writer.write("bool m_requestIdHasBeenSet = false;"); + } } }); writer.write(""); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java new file mode 100644 index 00000000000..0b4bf2a5e11 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java @@ -0,0 +1,86 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.protocol.ProtocolTraits; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EventPayloadRenderer; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.EventPayloadTrait; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies {@link EventPayloadRenderer} produces a header-only blob-carrier event (C2J + * {@code EventHeader.vm} form): an {@code Aws::Vector} payload with a bytes + * constructor, non-template accessors, and a {@code GetWithOwnership()} move-out — and + * NO JSON serde and NO {@code .cpp}. + */ +class EventPayloadRendererTest { + + private static Model model() { + BlobShape blob = BlobShape.builder().id("com.example#Blob").build(); + StructureShape event = StructureShape.builder() + .id("com.example#InvokeResponseStreamUpdate") + .addTrait(new DocumentationTrait("

A chunk of the streamed response payload.

")) + .addMember(MemberShape.builder() + .id("com.example#InvokeResponseStreamUpdate$Payload").target(blob.getId()) + .addTrait(new EventPayloadTrait()) + .addTrait(new DocumentationTrait("

Data returned by your Lambda function.

")) + .build()) + .build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2015-03-31").build(); + return Model.builder().addShapes(blob, event, service).build(); + } + + private static String renderHeader() { + Model model = model(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + ProtocolTraits traits = ProtocolResolver.traitsFor(Protocol.JSON); + new EventPayloadRenderer( + java.util.List.of(model.expectShape(ShapeId.from("com.example#InvokeResponseStreamUpdate"))), + new RenderContext(model, service, traits, "Lambda", "AWS_LAMBDA_API", "lambda")) + .render(delegator); + delegator.flushWriters(); + java.util.Map out = new java.util.TreeMap<>(); + for (java.nio.file.Path path : manifest.getFiles()) { + out.put(path.getFileName().toString(), manifest.getFileString(path).orElseThrow()); + } + assertFalse(out.containsKey("InvokeResponseStreamUpdate.cpp"), + "Blob-payload event must be header-only (no .cpp): " + out.keySet()); + return out.get("InvokeResponseStreamUpdate.h"); + } + + @Test + void rendersBlobCarrierHeader() { + String h = renderHeader(); + assertTrue(h.contains("Aws::Vector"), h); + assertTrue(h.contains("InvokeResponseStreamUpdate(Aws::Vector&& value)"), h); + assertTrue(h.contains("GetPayloadWithOwnership"), h); + assertTrue(h.contains("bool m_payloadHasBeenSet = false;"), h); + // Payload member documentation flows through. + assertTrue(h.contains("Data returned by your Lambda function."), h); + } + + @Test + void hasNoJsonSerde() { + String h = renderHeader(); + assertFalse(h.contains("Jsonize"), h); + assertFalse(h.contains("JsonView"), h); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java index 7bd57f1e0cc..598095009a8 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java @@ -394,4 +394,76 @@ void renderRequestIdAccessors_emitsTemplatedGetSetWith() { assertTrue(out.contains("///@{")); assertTrue(out.contains("///@}")); } + + /** + * A structure carrying: the framework ResponseMetadata envelope member (targeting a + * ResponseMetadata structure), a modeled {@code @required} member, and a plain member. + */ + private static Model responseMetadataModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape responseMetadata = StructureShape.builder() + .id("com.example#ResponseMetadata") + .addMember(MemberShape.builder() + .id("com.example#ResponseMetadata$RequestId").target(str.getId()).build()) + .build(); + StructureShape shape = StructureShape.builder() + .id("com.example#MyShape") + .addMember(MemberShape.builder() + .id("com.example#MyShape$ResponseMetadata").target(responseMetadata.getId()).build()) + .addMember(MemberShape.builder() + .id("com.example#MyShape$RequiredName").target(str.getId()) + .addTrait(new software.amazon.smithy.model.traits.RequiredTrait()).build()) + .addMember(MemberShape.builder() + .id("com.example#MyShape$Name").target(str.getId()).build()) + .build(); + return Model.builder().addShapes(str, responseMetadata, shape).build(); + } + + private static StructureShape myShape(Model model) { + return model.expectShape( + software.amazon.smithy.model.shapes.ShapeId.from("com.example#MyShape"), StructureShape.class); + } + + @Test + void injectedResponseMetadata_inStructure_omitsGetter_whileOthersKeepIt() { + // The injected ResponseMetadata envelope is always present -> no HasBeenSet getter. A + // modeled @required member is NOT special-cased (C2J clears required), so it keeps its + // getter just like a plain member. + Model model = responseMetadataModel(); + CppWriter writer = new CppWriter(); + MemberRenderer.forStructure(model, myShape(model), "MyShape").renderPublicAccessors(writer); + String out = writer.toString(); + assertFalse(out.contains("ResponseMetadataHasBeenSet()"), + "injected ResponseMetadata must not get a HasBeenSet getter: " + out); + assertTrue(out.contains("inline bool RequiredNameHasBeenSet() const"), + "modeled @required member must still get a HasBeenSet getter: " + out); + assertTrue(out.contains("inline bool NameHasBeenSet() const"), + "plain member must get a HasBeenSet getter: " + out); + } + + @Test + void injectedResponseMetadata_inStructure_initsFlagTrue_whileOthersFalse() { + Model model = responseMetadataModel(); + CppWriter writer = new CppWriter(); + MemberRenderer.forStructure(model, myShape(model), null).renderHasBeenSetFlags(writer); + String out = writer.toString(); + assertTrue(out.contains("bool m_responseMetadataHasBeenSet = true;"), + "injected ResponseMetadata flag must init true: " + out); + assertTrue(out.contains("bool m_requiredNameHasBeenSet = false;"), + "modeled @required member flag must init false: " + out); + assertTrue(out.contains("bool m_nameHasBeenSet = false;"), + "plain member flag must init false: " + out); + } + + @Test + void injectedResponseMetadata_inResult_initsFlagFalse() { + // Results use useRequiredField=false (emitHasBeenSet=false), so even the injected + // ResponseMetadata inits false there (and results emit no HasBeenSet getters). + Model model = responseMetadataModel(); + CppWriter writer = new CppWriter(); + MemberRenderer.forResult(model, myShape(model), null).renderHasBeenSetFlags(writer); + String out = writer.toString(); + assertTrue(out.contains("bool m_responseMetadataHasBeenSet = false;"), + "injected ResponseMetadata flag stays false in a result context: " + out); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java index 7c709a1e062..d00b8c63544 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java @@ -423,6 +423,153 @@ void noInputOperation_stillProducesRequest() { "Expected a RequestInfo for the no-input operation Ping"); } + /** + * A structure that is BOTH an operation output AND referenced as a member (via a list + * element). Mirrors Lambda's FunctionConfiguration, which is the output of + * GetFunctionConfiguration and also the element of FunctionList / a member of another + * response. Such a dual-role shape must end up in BOTH results (per-op output) and + * subObjects (standalone model file), matching C2J. + */ + private Model buildDualRoleModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + // Thing is the output of GetThing AND the element of ThingList. + StructureShape thing = StructureShape.builder() + .id("com.example#Thing") + .addMember("name", str.getId()) + .build(); + ListShape thingList = ListShape.builder() + .id("com.example#ThingList") + .member(thing.getId()) + .build(); + StructureShape getThingRequest = StructureShape.builder() + .id("com.example#GetThingRequest") + .addMember("id", str.getId()) + .build(); + StructureShape listThingsRequest = StructureShape.builder() + .id("com.example#ListThingsRequest") + .build(); + StructureShape listThingsResponse = StructureShape.builder() + .id("com.example#ListThingsResponse") + .addMember("things", thingList.getId()) + .build(); + OperationShape getThing = OperationShape.builder() + .id("com.example#GetThing") + .input(getThingRequest.getId()) + .output(thing.getId()) + .build(); + OperationShape listThings = OperationShape.builder() + .id("com.example#ListThings") + .input(listThingsRequest.getId()) + .output(listThingsResponse.getId()) + .build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService") + .version("2023-01-01") + .addOperation(getThing.getId()) + .addOperation(listThings.getId()) + .addTrait(ServiceTrait.builder().sdkId("test").arnNamespace("test").cloudFormationName("Test").cloudTrailEventSource("test").build()) + .build(); + return Model.builder() + .addShapes(str, thing, thingList, getThingRequest, listThingsRequest, listThingsResponse, getThing, listThings, service) + .build(); + } + + @Test + void dualRoleOutputAndMember_appearsInBothResultsAndSubObjects() { + Model model = buildDualRoleModel(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + assertTrue(classified.results().stream() + .anyMatch(r -> r.shape().getId().getName().equals("Thing")), + "Dual-role Thing must be classified as a result (GetThing output): " + classified.results()); + assertTrue(classified.subObjects().stream() + .anyMatch(s -> s.getId().getName().equals("Thing")), + "Dual-role Thing must also be classified as a sub-object (referenced via ThingList): " + + classified.subObjects()); + } + + @Test + void outputOnly_appearsInResultsButNotSubObjects() { + // GetItemResponse is ONLY an operation output; it is not referenced as a member anywhere. + Model model = buildSimpleModel(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + assertTrue(classified.results().stream() + .anyMatch(r -> r.shape().getId().getName().equals("GetItemResponse")), + "Output-only GetItemResponse must be in results"); + assertTrue(classified.subObjects().stream() + .noneMatch(s -> s.getId().getName().equals("GetItemResponse")), + "Output-only GetItemResponse must NOT be over-emitted as a sub-object: " + classified.subObjects()); + } + + /** + * A @streaming union with two event members: one whose sole payload is an @eventPayload blob + * (like Lambda's InvokeResponseStreamUpdate / the PayloadChunk member) and one with only a + * plain string member (like a CompleteEvent). Bound to an operation output so both event + * structs are reachable. + */ + private Model eventStreamBlobPayloadModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + BlobShape blob = BlobShape.builder().id("com.example#Blob").build(); + // Blob-payload event: single @eventPayload blob member. + StructureShape updateEvent = StructureShape.builder() + .id("com.example#UpdateEvent") + .addMember(MemberShape.builder() + .id("com.example#UpdateEvent$Payload").target(blob.getId()) + .addTrait(new EventPayloadTrait()).build()) + .build(); + // Non-blob event: plain string member -> must stay a sub-object. + StructureShape completeEvent = StructureShape.builder() + .id("com.example#CompleteEvent") + .addMember("Details", str.getId()) + .build(); + UnionShape eventStream = UnionShape.builder() + .id("com.example#ResponseStreamEvent") + .addTrait(new StreamingTrait()) + .addMember("PayloadChunk", updateEvent.getId()) + .addMember("Complete", completeEvent.getId()) + .build(); + StructureShape request = StructureShape.builder() + .id("com.example#InvokeRequest").addMember("name", str.getId()).build(); + StructureShape response = StructureShape.builder() + .id("com.example#InvokeResponse").addMember("events", eventStream.getId()).build(); + OperationShape op = OperationShape.builder() + .id("com.example#Invoke").input(request.getId()).output(response.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2023-01-01").addOperation(op.getId()) + .addTrait(ServiceTrait.builder().sdkId("test").arnNamespace("test").cloudFormationName("Test").cloudTrailEventSource("test").build()) + .build(); + return Model.builder() + .addShapes(str, blob, updateEvent, completeEvent, eventStream, request, response, op, service) + .build(); + } + + @Test + void blobEventPayload_isClassifiedAsBlobPayloadEventNotSubObject() { + Model model = eventStreamBlobPayloadModel(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + assertTrue(classified.blobPayloadEvents().stream() + .anyMatch(s -> s.getId().getName().equals("UpdateEvent")), + "Blob @eventPayload event must be in blobPayloadEvents: " + classified.blobPayloadEvents()); + assertTrue(classified.subObjects().stream() + .noneMatch(s -> s.getId().getName().equals("UpdateEvent")), + "Blob @eventPayload event must NOT be a sub-object: " + classified.subObjects()); + } + + @Test + void nonBlobEvent_staysSubObject() { + Model model = eventStreamBlobPayloadModel(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + assertTrue(classified.subObjects().stream() + .anyMatch(s -> s.getId().getName().equals("CompleteEvent")), + "Non-blob event struct must stay a sub-object: " + classified.subObjects()); + assertTrue(classified.blobPayloadEvents().stream() + .noneMatch(s -> s.getId().getName().equals("CompleteEvent")), + "Non-blob event struct must NOT be a blob-payload event: " + classified.blobPayloadEvents()); + } + @Test void classifiesEnumShape() { // StringShape with @enum trait -> classified as enum diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java index 9052f7abd91..6bd4b31f34e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StringShape; @@ -19,6 +20,7 @@ import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.model.traits.Trait; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -168,4 +170,82 @@ void streamingUnion_isNotRenderedBySubObjectRenderer() { assertFalse(files.containsKey("BidirectionalInput.cpp"), "Streaming union must NOT be rendered by SubObjectRenderer: " + files.keySet()); } + + // --- dual-role (operation output that is also a member) requestId stamp --- + + /** + * A model where {@code Thing} is BOTH the output of {@code DoThing} AND a member of {@code Plain} + * (dual-role: an output referenced as a member). {@code Plain} is a plain member-only sub-object. + * The service carries the given protocol trait. + */ + private static Model dualRoleModel(Trait protocolTrait) { + StringShape str = StringShape.builder().id("com.example#Str").build(); + StructureShape thing = StructureShape.builder() + .id("com.example#Thing").addMember("name", str.getId()).build(); + // Plain references Thing (making Thing a member target) and is itself a member of the input, + // so both Plain and Thing are reachable sub-objects; only Thing is an operation output. + StructureShape plain = StructureShape.builder() + .id("com.example#Plain") + .addMember("label", str.getId()) + .addMember("thing", thing.getId()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoThingInput").addMember("plain", plain.getId()).build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoThing").input(input.getId()).output(thing.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(protocolTrait).addOperation(op.getId()).build(); + return Model.builder().addShapes(str, thing, plain, input, op, service).build(); + } + + /** Classifies {@code dualRoleModel} and renders its sub-objects, returning filename -> content. */ + private static java.util.Map renderDualRole(Trait protocolTrait) { + Model model = dualRoleModel(protocolTrait); + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + Protocol protocol = ProtocolResolver.resolve(service, model); + ShapeClassifier.ClassifiedShapes classified = ShapeClassifier.classify(model, service, protocol); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + new SubObjectRenderer(classified.subObjects(), classified.resultOutputIds(), + new RenderContext(model, service, ProtocolResolver.traitsFor(protocol), + "Example", "AWS_EXAMPLE_API", "example")).render(delegator); + delegator.flushWriters(); + java.util.Map out = new java.util.TreeMap<>(); + for (java.nio.file.Path path : manifest.getFiles()) { + out.put(path.getFileName().toString(), manifest.getFileString(path).orElseThrow()); + } + return out; + } + + @Test + void dualRoleOutput_jsonProtocol_stampsRequestId() { + String h = renderDualRole( + software.amazon.smithy.aws.traits.protocols.RestJson1Trait.builder().build()).get("Thing.h"); + assertTrue(h.contains("inline const Aws::String& GetRequestId() const { return m_requestId; }"), h); + assertTrue(h.contains("inline bool RequestIdHasBeenSet() const { return m_requestIdHasBeenSet; }"), h); + assertTrue(h.contains("Aws::String m_requestId;"), h); + assertTrue(h.contains("bool m_requestIdHasBeenSet = false;"), h); + // The stamped Aws::String field pulls in AWSString.h. + assertTrue(h.contains("#include "), h); + } + + @Test + void memberOnlySubObject_jsonProtocol_hasNoRequestId() { + String h = renderDualRole( + software.amazon.smithy.aws.traits.protocols.RestJson1Trait.builder().build()).get("Plain.h"); + assertFalse(h.contains("GetRequestId"), + "A member-only (non-output) sub-object must not receive the requestId stamp: " + h); + assertFalse(h.contains("m_requestId"), h); + } + + @Test + void dualRoleOutput_queryProtocol_hasNoRequestId() { + // Query/EC2 dual-role outputs get a ResponseMetadata member instead (injectResponseMetadata), + // so resultHasTopLevelRequestId() is false and no requestId block is stamped here. + String h = renderDualRole( + new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait()).get("Thing.h"); + assertFalse(h.contains("GetRequestId"), + "Query dual-role output must not receive the requestId stamp: " + h); + } } From 34f7dcebb8e5de98ddc809bafae33866e823239f Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 24 Aug 2026 13:36:26 -0400 Subject: [PATCH 06/44] Smithy: EC2 transforms fast-fail on Result/Response and SecureBlob collisions Smithy: renameMember fast-fails on member collision, adds jsonName overload --- .../model/transforms/Ec2Transforms.java | 32 ++++++---- .../model/transforms/TransformSupport.java | 35 +++++++++-- .../model/transforms/Ec2TransformsTest.java | 43 ++++++------- .../transforms/TransformSupportTest.java | 60 +++++++++++++++++++ 4 files changed, 134 insertions(+), 36 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java index 6a007880881..49d7cd1607a 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java @@ -37,9 +37,11 @@ * ({@code ec2//service-2.json}) marks {@code UserData} sensitive via * {@code SecureBlobAttributeValue -> SecureBlob (@sensitive)}, but the upstream Smithy model * ({@code ec2/smithy/model.json}) still targets the non-sensitive {@code BlobAttributeValue}. This - * transform mirrors the C2J modeling in the Smithy model so generated code matches. It self-retires - * (no-op) once the upstream Smithy model catches up, and is a temporary compensation for that - * upstream data lag — see docs/superpowers/plans/parity-deltas.md. + * transform mirrors the C2J modeling in the Smithy model so generated code matches. It is a + * temporary compensation for that upstream data lag; once the upstream Smithy model catches up and + * already defines {@code SecureBlobAttributeValue}, this transform throws {@code IllegalStateException} + * so a human removes it rather than letting it silently self-retire — see + * docs/superpowers/plans/parity-deltas.md. */ public final class Ec2Transforms { @@ -64,9 +66,11 @@ private static Model apply(Model model, ServiceShape service) { * {@code BlobAttributeValue}; after repointing, {@code BlobAttributeValue} is no longer * referenced and drops out of the reachable (emitted) set, exactly as it does in C2J. * - *

No-op — leaving the model untouched — when {@code SecureBlobAttributeValue} already exists - * (upstream Smithy caught up) or {@code UserData} no longer targets {@code BlobAttributeValue}, - * so the transform cannot introduce a duplicate shape or fight a corrected upstream model. + *

Throws {@code IllegalStateException} when {@code SecureBlobAttributeValue} already exists + * (upstream Smithy caught up), signalling this compensating transform is obsolete and must be + * removed. No-op — leaving the model untouched — when {@code ModifyInstanceAttributeRequest} or + * its {@code UserData} member is absent, or {@code UserData} no longer targets + * {@code BlobAttributeValue} (source-absent, not a collision). */ private static Model addSecureBlobUserData(Model model) { Optional requestOpt = model.shapes(StructureShape.class) @@ -86,8 +90,13 @@ private static Model addSecureBlobUserData(Model model) { ShapeId secureStructId = ShapeId.fromParts(namespace, "SecureBlobAttributeValue"); ShapeId blobAttrId = ShapeId.fromParts(namespace, "BlobAttributeValue"); - if (model.getShape(secureStructId).isPresent() || !userData.getTarget().equals(blobAttrId)) { - return model; + if (model.getShape(secureStructId).isPresent()) { + throw new IllegalStateException("EC2 SecureBlobAttributeValue already exists in the model; " + + "the upstream Smithy model has caught up and this compensating transform is obsolete " + + "and must be removed."); + } + if (!userData.getTarget().equals(blobAttrId)) { + return model; // UserData no longer targets BlobAttributeValue: nothing to repoint (no-op). } MemberShape originalValue = model.expectShape(blobAttrId, StructureShape.class) .getAllMembers().get("Value"); @@ -122,9 +131,12 @@ private static Model renameResultShapesToResponse(Model model) { if (name.endsWith("Result")) { String target = name.substring(0, name.length() - "Result".length()) + "Response"; ShapeId targetId = ShapeId.fromParts(shape.getId().getNamespace(), target); - if (!model.getShape(targetId).isPresent()) { - renames.put(shape.getId(), targetId); + if (model.getShape(targetId).isPresent()) { + throw new IllegalStateException("EC2 *Result->*Response rename collision: '" + + targetId + "' already exists (would clobber '" + shape.getId() + + "'). Upstream model likely changed; review the EC2 transform."); } + renames.put(shape.getId(), targetId); } } if (renames.isEmpty()) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java index c82fe57454d..dd1d5aa35aa 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java @@ -81,18 +81,43 @@ static Optional appendValues(Shape enumShape, List values) { /** * Returns a copy of {@code struct} with member {@code oldName} renamed to {@code newName}, * preserving member declaration order and copying all traits onto the renamed member. Returns - * {@link Optional#empty()} if {@code oldName} is absent or {@code newName} already exists. + * {@link Optional#empty()} if {@code oldName} is absent (nothing to rename). + * + * @throws IllegalStateException if {@code newName} is already a distinct member — a genuine + * collision that would silently drop a member. Callers must not mask this. */ static Optional renameMember(StructureShape struct, String oldName, String newName) { - if (struct.getMember(oldName).isEmpty() || struct.getMember(newName).isPresent()) { + return renameMember(struct, oldName, newName, new software.amazon.smithy.model.traits.Trait[0]); + } + + /** + * As {@link #renameMember(StructureShape, String, String)}, additionally attaching + * {@code extraTraits} to the renamed member (e.g. a {@code @jsonName} to preserve the original + * wire name when the C++ member name changes). Existing member traits are copied first, then the + * extras are added. + */ + static Optional renameMember(StructureShape struct, String oldName, String newName, + software.amazon.smithy.model.traits.Trait... extraTraits) { + if (struct.getMember(oldName).isEmpty()) { return Optional.empty(); } + if (struct.getMember(newName).isPresent()) { + throw new IllegalStateException("Cannot rename member '" + oldName + "' to '" + newName + + "' on " + struct.getId() + ": a distinct '" + newName + "' member already exists"); + } StructureShape.Builder builder = StructureShape.builder().id(struct.getId()); struct.getAllTraits().values().forEach(builder::addTrait); for (MemberShape member : struct.getAllMembers().values()) { - String name = member.getMemberName().equals(oldName) ? newName : member.getMemberName(); - builder.addMember(name, member.getTarget(), - b -> member.getAllTraits().values().forEach(b::addTrait)); + boolean isTarget = member.getMemberName().equals(oldName); + String name = isTarget ? newName : member.getMemberName(); + builder.addMember(name, member.getTarget(), b -> { + member.getAllTraits().values().forEach(b::addTrait); + if (isTarget) { + for (software.amazon.smithy.model.traits.Trait t : extraTraits) { + b.addTrait(t); + } + } + }); } return Optional.of(builder.build()); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java index 0dbe9184ba3..0f0e352e7c9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java @@ -72,18 +72,23 @@ void renamesNestedResultStructToResponse() { } @Test - void collisionGuardLeavesResultUnchangedWhenResponseExists() { - StructureShape resultShape = StructureShape.builder() - .id("com.example#FooResult").build(); - StructureShape responseShape = StructureShape.builder() - .id("com.example#FooResponse").build(); - ServiceShape service = ec2Service("EC2"); - Model m = Model.assembler().addShapes(resultShape, responseShape, service).assemble().unwrap(); - - Model out = Ec2Transforms.asTransform().apply(m, service); - - assertTrue(out.getShape(ShapeId.from("com.example#FooResult")).isPresent()); - assertTrue(out.getShape(ShapeId.from("com.example#FooResponse")).isPresent()); + void throwsWhenResponseShapeAlreadyExists() { + // A *Result domain struct colliding with an existing *Response is a genuine collision: + // fail loudly rather than silently skip (the obsolete-transform / drift signal). + StructureShape result = StructureShape.builder().id("com.example#FooResult").build(); + StructureShape response = StructureShape.builder().id("com.example#FooResponse").build(); + StructureShape in = StructureShape.builder().id("com.example#DescribeThingsRequest").build(); + StructureShape out = StructureShape.builder().id("com.example#DescribeThingsResult").build(); + OperationShape op = OperationShape.builder().id("com.example#DescribeThings") + .input(in.getId()).output(out.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(ServiceTrait.builder().sdkId("EC2").arnNamespace("ec2") + .cloudFormationName("EC2").cloudTrailEventSource("ec2").build()) + .addOperation(op.getId()).build(); + Model m = Model.assembler().addShapes(result, response, in, out, op, service).assemble().unwrap(); + assertThrows(IllegalStateException.class, + () -> Ec2Transforms.asTransform().apply(m, service(m))); } /** @@ -144,15 +149,11 @@ void modelsUserDataAsSensitiveSecureBlobAttributeValue() { } @Test - void secureBlobUserDataTransformIsIdempotent() { - // Re-applying must not throw or duplicate shapes: once SecureBlobAttributeValue exists the - // transform self-retires, so it is safe if the upstream Smithy model later adds the shape. + void throwsWhenSecureBlobAttributeValueAlreadyExists() { + // Once upstream aws-models adds SecureBlobAttributeValue, this compensating transform is + // obsolete. Fail loudly so a human removes it, rather than silently self-retiring. Model once = Ec2Transforms.asTransform().apply(userDataModel(), service(userDataModel())); - Model twice = Ec2Transforms.asTransform().apply(once, service(once)); - - MemberShape userData = twice.expectShape( - ShapeId.from("com.example#ModifyInstanceAttributeRequest"), StructureShape.class) - .getAllMembers().get("UserData"); - assertEquals(ShapeId.from("com.example#SecureBlobAttributeValue"), userData.getTarget()); + assertThrows(IllegalStateException.class, + () -> Ec2Transforms.asTransform().apply(once, service(once))); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java new file mode 100644 index 00000000000..1bc07bf6656 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java @@ -0,0 +1,60 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.JsonNameTrait; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class TransformSupportTest { + + private static StructureShape struct(String... memberNames) { + StructureShape.Builder b = StructureShape.builder().id("com.example#Req"); + for (String m : memberNames) { + b.addMember(m, ShapeId.from("smithy.api#String")); + } + return b.build(); + } + + @Test + void renameMember_sourceAbsent_returnsEmpty() { + assertTrue(TransformSupport.renameMember(struct("name"), "body", "requestBody").isEmpty()); + } + + @Test + void renameMember_targetExists_throws() { + StructureShape s = struct("body", "requestBody"); + assertThrows(IllegalStateException.class, + () -> TransformSupport.renameMember(s, "body", "requestBody")); + } + + @Test + void renameMember_success_renamesAndPreservesOrder() { + StructureShape s = struct("a", "body", "z"); + StructureShape out = TransformSupport.renameMember(s, "body", "requestBody").orElseThrow(); + assertFalse(out.getMember("body").isPresent()); + assertTrue(out.getMember("requestBody").isPresent()); + List order = new ArrayList<>(out.getAllMembers().keySet()); + assertEquals(List.of("a", "requestBody", "z"), order); + } + + @Test + void renameMember_withJsonName_attachesTraitToRenamedMember() { + StructureShape s = struct("generatedPolicyResult"); + StructureShape out = TransformSupport.renameMember( + s, "generatedPolicyResult", "generatedPolicyResults", + new JsonNameTrait("generatedPolicyResult")).orElseThrow(); + MemberShape renamed = out.getMember("generatedPolicyResults").orElseThrow(); + assertEquals("generatedPolicyResult", renamed.expectTrait(JsonNameTrait.class).getValue()); + } +} From 4b8e193a4e381bd8c26df00c48f50bf17843f945 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 24 Aug 2026 13:47:22 -0400 Subject: [PATCH 07/44] Smithy: wire reserved request-member rename with raw-smithy-name skip-lists --- .../model/transforms/GlobalTransforms.java | 98 ++++++++++----- .../model/GlobalTransformsTest.java | 118 ++++++++++-------- 2 files changed, 129 insertions(+), 87 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java index 926e51068f3..29879827cf7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java @@ -4,6 +4,7 @@ */ package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; @@ -26,6 +27,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -37,19 +39,21 @@ public final class GlobalTransforms { /** - * Services that skip the "body" -> "requestBody" member rename. - * These services use "body" as a meaningful domain member (e.g., HTTP payload). + * Services that skip the "body" -> "requestBody" member rename (raw smithy service names). + * These use "body" as a meaningful domain/payload member. API Gateway (api-gateway) and + * API Gateway V2 (apigatewayv2) are skipped here because their dedicated transforms own the + * rename; the rest use "body" as an HTTP payload. */ private static final Set BODY_RENAME_SKIP_SERVICES = Set.of( - "amplifyuibuilder", "apigateway", "apigateway2", "bedrock-runtime", "glacier", "repostspace" + "amplifyuibuilder", "api-gateway", "apigatewayv2", "bedrock-runtime", "glacier", "repostspace" ); /** - * Services that skip the "headers" -> "headerValues" member rename. - * These services use "headers" as a meaningful domain member. + * Services that skip the "headers" -> "headerValues" member rename (raw smithy service name). + * api-gateway renames headers to "requestHeaders" in its dedicated transform instead. */ private static final Set HEADERS_RENAME_SKIP_SERVICES = Set.of( - "apigateway" + "api-gateway" ); /** @@ -62,37 +66,63 @@ public final class GlobalTransforms { private GlobalTransforms() {} - // NOTE: This reserved-member rename is intentionally NOT wired into the transform - // pipeline yet. It encodes a known C2J-parity requirement (body -> requestBody, - // headers -> headerValues) that is validated by GlobalTransformsTest but not applied - // during generation. Do not delete it and do not hook it up as part of a cleanup pass — - // wiring it in changes generated output and must be its own reviewed change. /** - * Returns the renamed C++ member name if this member is reserved. - * Only applies to request shape members (caller must filter). + * Renames reserved request members on every operation-input structure: {@code body -> + * requestBody}, {@code headers -> headerValues}, {@code Headers -> headerValues}, honoring the + * per-service skip-lists. Mirrors the legacy C2J {@code RESERVED_REQUEST_MEMBER_MAPPING}. Only + * operation-input shapes are touched (never arbitrary domain shapes that happen to end in + * "Request"). A collision (the target member name already present) throws via + * {@link TransformSupport#renameMember}. * - * Reserved members and their renames: - * - "body" -> "requestBody" (unless service is in BODY_RENAME_SKIP_SERVICES) - * - "headers" -> "headerValues" (unless service is in HEADERS_RENAME_SKIP_SERVICES) - * - "Headers" -> "headerValues" (always renamed, no skip list) - * - * @param memberName the original member name from the model - * @param smithyServiceName the service name (lowercase hyphenated, e.g., "bedrock-runtime") - * @return the renamed member name, or empty if no rename is needed + * @param model the current model + * @param service the service being generated (its raw smithy name drives the skip-lists) + * @return the model with reserved input members renamed, or the input model if none applied */ - public static Optional getReservedMemberRename(String memberName, String smithyServiceName) { - if ("body".equals(memberName)) { - if (BODY_RENAME_SKIP_SERVICES.contains(smithyServiceName)) return Optional.empty(); - return Optional.of("requestBody"); + static Model renameReservedRequestMembers(Model model, ServiceShape service) { + String smithyServiceName = ServiceNameUtil.getSmithyServiceName(service, null); + Set inputIds = new HashSet<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + inputIds.add(op.getInputShape()); + } + List updated = new ArrayList<>(); + for (ShapeId inputId : inputIds) { + model.getShape(inputId).flatMap(Shape::asStructureShape).ifPresent(struct -> { + StructureShape current = struct; + boolean changed = false; + for (Map.Entry rename : reservedRenames(current, smithyServiceName)) { + Optional next = + TransformSupport.renameMember(current, rename.getKey(), rename.getValue()); + if (next.isPresent()) { + current = next.get(); + changed = true; + } + } + if (changed) { + updated.add(current); + } + }); + } + if (updated.isEmpty()) { + return model; + } + return model.toBuilder().addShapes(updated).build(); + } + + /** Ordered (oldName -> newName) reserved-member renames applicable to this input struct. */ + private static List> reservedRenames(StructureShape struct, + String smithyServiceName) { + List> out = new ArrayList<>(); + if (struct.getMember("body").isPresent() && !BODY_RENAME_SKIP_SERVICES.contains(smithyServiceName)) { + out.add(Map.entry("body", "requestBody")); } - if ("headers".equals(memberName)) { - if (HEADERS_RENAME_SKIP_SERVICES.contains(smithyServiceName)) return Optional.empty(); - return Optional.of("headerValues"); + if (struct.getMember("headers").isPresent() + && !HEADERS_RENAME_SKIP_SERVICES.contains(smithyServiceName)) { + out.add(Map.entry("headers", "headerValues")); } - if ("Headers".equals(memberName)) { - return Optional.of("headerValues"); + if (struct.getMember("Headers").isPresent()) { + out.add(Map.entry("Headers", "headerValues")); } - return Optional.empty(); + return out; } /** @@ -125,10 +155,12 @@ private static void addReachableFrom(ShapeId root, Walker walker, Model model, S * Returns this class as a ModelTransform. * *

Runs {@link #dropDeprecatedMembers} first (so reachability filtering sees the pruned - * model and orphaned targets drop out), then {@link #injectResponseMetadata}. + * model and orphaned targets drop out), then {@link #renameReservedRequestMembers} to apply the + * C2J-parity request-member renames, then {@link #injectResponseMetadata}. */ public static ModelTransform asTransform() { - return (model, service) -> injectResponseMetadata(dropDeprecatedMembers(model, service), service); + return (model, service) -> injectResponseMetadata( + renameReservedRequestMembers(dropDeprecatedMembers(model, service), service), service); } /** diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index ffa9a1e1fdc..a1c388cda2b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java @@ -9,89 +9,99 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.*; -import java.util.Optional; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; class GlobalTransformsTest { - @Test - void reservedMemberRename_body_becomesRequestBody() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "kinesis"); - assertEquals(Optional.of("requestBody"), result); - } - - @Test - void reservedMemberRename_body_skippedForApiGateway() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "apigateway"); - assertTrue(result.isEmpty()); - } - - @Test - void reservedMemberRename_body_skippedForBedrockRuntime() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "bedrock-runtime"); - assertTrue(result.isEmpty()); - } - - @Test - void reservedMemberRename_body_skippedForAmplifyUiBuilder() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "amplifyuibuilder"); - assertTrue(result.isEmpty()); - } - - @Test - void reservedMemberRename_body_skippedForApiGateway2() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "apigateway2"); - assertTrue(result.isEmpty()); + /** One-operation service under sdkId with an input struct carrying the given members. */ + private static Model inputModel(String sdkId, String... inputMembers) { + StructureShape.Builder in = StructureShape.builder().id("com.example#DoThingRequest"); + for (String m : inputMembers) { + in.addMember(m, ShapeId.from("smithy.api#String")); + } + StructureShape input = in.build(); + StructureShape output = StructureShape.builder().id("com.example#DoThingResponse").build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoThing").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(software.amazon.smithy.aws.traits.ServiceTrait.builder() + .sdkId(sdkId).arnNamespace("x").cloudFormationName("X").cloudTrailEventSource("x").build()) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(input, output, op, service).assemble().unwrap(); } - @Test - void reservedMemberRename_body_skippedForGlacier() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "glacier"); - assertTrue(result.isEmpty()); + private static StructureShape input(Model m) { + return m.expectShape(ShapeId.from("com.example#DoThingRequest"), StructureShape.class); } @Test - void reservedMemberRename_body_skippedForRepostSpace() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "repostspace"); - assertTrue(result.isEmpty()); + void reservedRename_body_becomesRequestBody_forNonSkippedService() { + Model m = inputModel("Security IR", "body"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + assertFalse(input(out).getMember("body").isPresent()); + assertTrue(input(out).getMember("requestBody").isPresent()); } @Test - void reservedMemberRename_headers_becomesHeaderValues() { - Optional result = GlobalTransforms.getReservedMemberRename("headers", "kinesis"); - assertEquals(Optional.of("headerValues"), result); + void reservedRename_body_skippedForBedrockRuntime() { + Model m = inputModel("Bedrock Runtime", "body"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("body").isPresent(), "skip-listed service keeps body"); } @Test - void reservedMemberRename_headers_skippedForApiGateway() { - Optional result = GlobalTransforms.getReservedMemberRename("headers", "apigateway"); - assertTrue(result.isEmpty()); + void reservedRename_body_skippedForApiGateway_rawName() { + // C2J name is "apigateway" but the raw smithy name is "api-gateway"; the skip-list must use + // the raw name or API Gateway's dedicated transform gets pre-empted. + Model m = inputModel("API Gateway", "body"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("body").isPresent(), "api-gateway must be skipped"); } @Test - void reservedMemberRename_headers_notSkippedForBedrockRuntime() { - Optional result = GlobalTransforms.getReservedMemberRename("headers", "bedrock-runtime"); - assertEquals(Optional.of("headerValues"), result); + void reservedRename_headers_becomesHeaderValues_forNonSkippedService() { + Model m = inputModel("Kinesis", "headers"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("headerValues").isPresent()); + assertFalse(input(out).getMember("headers").isPresent()); } @Test - void reservedMemberRename_Headers_alwaysRenamed() { - Optional result = GlobalTransforms.getReservedMemberRename("Headers", "apigateway"); - assertEquals(Optional.of("headerValues"), result); + void reservedRename_capitalHeaders_alwaysRenamed() { + Model m = inputModel("API Gateway", "Headers"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("headerValues").isPresent()); } @Test - void reservedMemberRename_Headers_alwaysRenamed_anyService() { - Optional result = GlobalTransforms.getReservedMemberRename("Headers", "kinesis"); - assertEquals(Optional.of("headerValues"), result); + void reservedRename_onlyTouchesOperationInputs_notArbitraryShapes() { + // A non-input structure that happens to have a 'body' member must NOT be renamed. + StructureShape domain = StructureShape.builder().id("com.example#HttpThing") + .addMember("body", ShapeId.from("smithy.api#String")).build(); + StructureShape input = StructureShape.builder().id("com.example#DoThingRequest") + .addMember("thing", domain.getId()).build(); + StructureShape output = StructureShape.builder().id("com.example#DoThingResponse").build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoThing").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(software.amazon.smithy.aws.traits.ServiceTrait.builder() + .sdkId("Kinesis").arnNamespace("x").cloudFormationName("X").cloudTrailEventSource("x").build()) + .addOperation(op.getId()).build(); + Model m = Model.assembler().addShapes(domain, input, output, op, service).assemble().unwrap(); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + assertTrue(out.expectShape(ShapeId.from("com.example#HttpThing"), StructureShape.class) + .getMember("body").isPresent(), "domain shape body must not be renamed"); } @Test - void reservedMemberRename_normalMember_returnsEmpty() { - Optional result = GlobalTransforms.getReservedMemberRename("name", "kinesis"); - assertTrue(result.isEmpty()); + void reservedRename_collision_throws() { + Model m = inputModel("Kinesis", "body", "requestBody"); + assertThrows(IllegalStateException.class, + () -> GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example"))); } // --- computeReachableShapes tests --- From 1e0e6da772b77cdbdccabb80a7a71d8934bcd0d0 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 24 Aug 2026 13:53:36 -0400 Subject: [PATCH 08/44] Smithy: AccessAnalyzerTransforms renames GeneratedPolicyResult(s) with jsonName parity Smithy: drop superseded accessanalyzer + dead cloudsearchdomain collision-map entries --- .../generators/ShapeUtil.java | 8 +- .../generators/model/ModelCodegenPlugin.java | 4 +- .../transforms/AccessAnalyzerTransforms.java | 68 ++++++++++++++ .../model/ShapeUtilExtensionsTest.java | 11 --- .../AccessAnalyzerTransformsTest.java | 89 +++++++++++++++++++ 5 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java index d10a51b689b..c36f1c5d9d8 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java @@ -71,9 +71,11 @@ public class ShapeUtil { * Map: service-name -> Map of original-shape-name -> resolved-name */ private static final Map> HARDCODED_COLLISION_RESOLUTIONS = Map.of( - "s3", Map.of("CopyObjectResult", "CopyObjectResultDetails"), - "accessanalyzer", Map.of("GeneratedPolicyResult", "GeneratedPolicyResults"), - "cloudsearchdomain", Map.of("SearchResult", "SearchResultDetails") + // accessanalyzer GeneratedPolicyResult->GeneratedPolicyResults is handled by + // AccessAnalyzerTransforms (a model transform), not this render-time map. + // cloudsearchdomain SearchResult->SearchResultDetails is dead: the current model has no + // colliding SearchResult shape. The s3 entry stays for the deferred S3 transform work. + "s3", Map.of("CopyObjectResult", "CopyObjectResultDetails") ); /** diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index a9066c5fd9d..99024a729a3 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -6,6 +6,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.AccessAnalyzerTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayV2Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.Ec2Transforms; @@ -54,7 +55,8 @@ public void execute(PluginContext context) { SqsTransforms.asTransform(), ApiGatewayTransforms.asTransform(), ApiGatewayV2Transforms.asTransform(), - Ec2Transforms.asTransform() + Ec2Transforms.asTransform(), + AccessAnalyzerTransforms.asTransform() // Future: S3Transforms.asTransform(), etc. )); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java new file mode 100644 index 00000000000..9277e94ba86 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java @@ -0,0 +1,68 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.JsonNameTrait; +import software.amazon.smithy.model.transform.ModelTransformer; + +import java.util.Map; +import java.util.Optional; + +/** + * Access Analyzer model parity with the legacy C2J transformer, which resolves the collision + * between the {@code GetGeneratedPolicy} result wrapper and the domain shape + * {@code GeneratedPolicyResult} by renaming the domain shape (and its referencing member) to + * {@code GeneratedPolicyResults}. C2J preserves the JSON wire key ({@code generatedPolicyResult}) + * via {@code locationName}; this transform mirrors that with a {@code @jsonName} trait so the model + * stays serde-correct even though serde is currently stubbed. + * + *

Self-guards on the raw smithy service name {@code accessanalyzer} (transforms never remap). + * No-op when the domain shape is absent (upstream already clean). Throws if the target name + * {@code GeneratedPolicyResults} is already occupied by a distinct shape — a genuine collision. + */ +public final class AccessAnalyzerTransforms { + + private AccessAnalyzerTransforms() {} + + public static ModelTransform asTransform() { + return AccessAnalyzerTransforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"accessanalyzer".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + String ns = service.getId().getNamespace(); + ShapeId oldShape = ShapeId.fromParts(ns, "GeneratedPolicyResult"); + ShapeId newShape = ShapeId.fromParts(ns, "GeneratedPolicyResults"); + + if (model.getShape(oldShape).isEmpty()) { + return model; // upstream already renamed / removed the shape: nothing to do. + } + if (model.getShape(newShape).isPresent()) { + throw new IllegalStateException("AccessAnalyzer collision: '" + newShape + + "' already exists; cannot rename '" + oldShape + "' onto it."); + } + + Model renamed = ModelTransformer.create().renameShapes(model, Map.of(oldShape, newShape)); + + ShapeId respId = ShapeId.fromParts(ns, "GetGeneratedPolicyResponse"); + Optional resp = renamed.getShape(respId).flatMap(Shape::asStructureShape); + if (resp.isEmpty()) { + return renamed; + } + Optional updated = TransformSupport.renameMember( + resp.get(), "generatedPolicyResult", "generatedPolicyResults", + new JsonNameTrait("generatedPolicyResult")); + return updated.map(s -> renamed.toBuilder().addShape(s).build()).orElse(renamed); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java index 7f57785b441..5daec8ce043 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java @@ -19,17 +19,6 @@ void hardcodedCollisionResolution_s3CopyObjectResult() { ShapeUtil.getHardcodedResolution("s3", "CopyObjectResult")); } - @Test - void hardcodedCollisionResolution_accessAnalyzer() { - assertEquals(Optional.of("GeneratedPolicyResults"), - ShapeUtil.getHardcodedResolution("accessanalyzer", "GeneratedPolicyResult")); - } - - @Test - void hardcodedCollisionResolution_cloudSearchDomain() { - assertEquals(Optional.of("SearchResultDetails"), - ShapeUtil.getHardcodedResolution("cloudsearchdomain", "SearchResult")); - } @Test void hardcodedCollisionResolution_noMatch_returnsEmpty() { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java new file mode 100644 index 00000000000..8ad9cda0120 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java @@ -0,0 +1,89 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.JsonNameTrait; + +import static org.junit.jupiter.api.Assertions.*; + +class AccessAnalyzerTransformsTest { + + private static Model model(String sdkId, boolean withResultShape) { + String ns = "com.amazonaws.accessanalyzer"; + StructureShape.Builder respB = StructureShape.builder().id(ns + "#GetGeneratedPolicyResponse"); + StructureShape gpr = StructureShape.builder().id(ns + "#GeneratedPolicyResult") + .addMember("x", ShapeId.from("smithy.api#String")).build(); + if (withResultShape) { + respB.addMember("generatedPolicyResult", gpr.getId()); + } + StructureShape resp = respB.build(); + StructureShape req = StructureShape.builder().id(ns + "#GetGeneratedPolicyRequest").build(); + OperationShape op = OperationShape.builder().id(ns + "#GetGeneratedPolicy") + .input(req.getId()).output(resp.getId()).build(); + ServiceShape svc = ServiceShape.builder().id(ns + "#AccessAnalyzer").version("2019-11-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("access-analyzer") + .cloudFormationName("AccessAnalyzer").cloudTrailEventSource("access-analyzer").build()) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(req, resp, op, svc).addShape(gpr).assemble().unwrap(); + } + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.amazonaws.accessanalyzer#AccessAnalyzer"), ServiceShape.class); + } + + @Test + void renamesShapeAndMember_withJsonNamePreserved() { + Model m = model("AccessAnalyzer", true); + Model out = AccessAnalyzerTransforms.asTransform().apply(m, service(m)); + + assertTrue(out.getShape( + ShapeId.from("com.amazonaws.accessanalyzer#GeneratedPolicyResults")).isPresent(), + "shape renamed to GeneratedPolicyResults"); + assertFalse(out.getShape( + ShapeId.from("com.amazonaws.accessanalyzer#GeneratedPolicyResult")).isPresent(), + "old shape name gone"); + + StructureShape resp = out.expectShape( + ShapeId.from("com.amazonaws.accessanalyzer#GetGeneratedPolicyResponse"), StructureShape.class); + MemberShape member = resp.getMember("generatedPolicyResults").orElseThrow(); + assertEquals("com.amazonaws.accessanalyzer#GeneratedPolicyResults", + member.getTarget().toString(), "member repointed to renamed shape"); + assertEquals("generatedPolicyResult", + member.expectTrait(JsonNameTrait.class).getValue(), "wire name preserved"); + assertFalse(resp.getMember("generatedPolicyResult").isPresent(), "old member name gone"); + } + + @Test + void noOpForOtherService() { + Model m = model("SomethingElse", true); + Model out = AccessAnalyzerTransforms.asTransform().apply(m, service(m)); + assertSame(m, out); + } + + @Test + void throwsWhenTargetShapeAlreadyExists() { + String ns = "com.amazonaws.accessanalyzer"; + StructureShape gpr = StructureShape.builder().id(ns + "#GeneratedPolicyResult") + .addMember("x", ShapeId.from("smithy.api#String")).build(); + StructureShape gprs = StructureShape.builder().id(ns + "#GeneratedPolicyResults") + .addMember("y", ShapeId.from("smithy.api#String")).build(); + StructureShape resp = StructureShape.builder().id(ns + "#GetGeneratedPolicyResponse") + .addMember("generatedPolicyResult", gpr.getId()).build(); + StructureShape req = StructureShape.builder().id(ns + "#GetGeneratedPolicyRequest").build(); + OperationShape op = OperationShape.builder().id(ns + "#GetGeneratedPolicy") + .input(req.getId()).output(resp.getId()).build(); + ServiceShape svc = ServiceShape.builder().id(ns + "#AccessAnalyzer").version("2019-11-01") + .addTrait(ServiceTrait.builder().sdkId("AccessAnalyzer").arnNamespace("access-analyzer") + .cloudFormationName("AccessAnalyzer").cloudTrailEventSource("access-analyzer").build()) + .addOperation(op.getId()).build(); + Model m = Model.assembler().addShapes(gpr, gprs, resp, req, op, svc).assemble().unwrap(); + assertThrows(IllegalStateException.class, + () -> AccessAnalyzerTransforms.asTransform().apply(m, service(m))); + } +} From 2df9ef7c9a3ac016a14f9e22b99a98c6ac041e1a Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 26 Aug 2026 16:48:43 -0400 Subject: [PATCH 09/44] Carry over serde traits when we rename members --- .../transforms/AccessAnalyzerTransforms.java | 14 +-- .../transforms/ApiGatewayTransforms.java | 7 +- .../transforms/ApiGatewayV2Transforms.java | 5 +- .../model/transforms/GlobalTransforms.java | 16 +-- .../transforms/SourceRegionTransform.java | 24 ++--- .../model/transforms/TransformSupport.java | 96 ++++++++++++++--- .../model/GlobalTransformsTest.java | 63 +++++++++++ .../transforms/ApiGatewayTransformsTest.java | 5 + .../ApiGatewayV2TransformsTest.java | 3 + .../transforms/TransformSupportTest.java | 100 ++++++++++++++++-- 10 files changed, 278 insertions(+), 55 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java index 9277e94ba86..94f8150efb7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java @@ -6,12 +6,13 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.traits.JsonNameTrait; import software.amazon.smithy.model.transform.ModelTransformer; import java.util.Map; @@ -21,9 +22,10 @@ * Access Analyzer model parity with the legacy C2J transformer, which resolves the collision * between the {@code GetGeneratedPolicy} result wrapper and the domain shape * {@code GeneratedPolicyResult} by renaming the domain shape (and its referencing member) to - * {@code GeneratedPolicyResults}. C2J preserves the JSON wire key ({@code generatedPolicyResult}) - * via {@code locationName}; this transform mirrors that with a {@code @jsonName} trait so the model - * stays serde-correct even though serde is currently stubbed. + * {@code GeneratedPolicyResults}. C2J preserves the wire key ({@code generatedPolicyResult}) via + * {@code locationName}; {@link TransformSupport#renameMember} mirrors that by pinning the original + * wire name through the service's protocol-appropriate trait, so the model stays serde-correct even + * though serde is currently stubbed. * *

Self-guards on the raw smithy service name {@code accessanalyzer} (transforms never remap). * No-op when the domain shape is absent (upstream already clean). Throws if the target name @@ -60,9 +62,9 @@ private static Model apply(Model model, ServiceShape service) { if (resp.isEmpty()) { return renamed; } + Protocol protocol = ProtocolResolver.resolve(service, renamed); Optional updated = TransformSupport.renameMember( - resp.get(), "generatedPolicyResult", "generatedPolicyResults", - new JsonNameTrait("generatedPolicyResult")); + resp.get(), "generatedPolicyResult", "generatedPolicyResults", protocol); return updated.map(s -> renamed.toBuilder().addShape(s).build()).orElse(renamed); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java index 221cadd326a..5aff1662174 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java @@ -6,6 +6,8 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; @@ -31,14 +33,15 @@ private static Model apply(Model model, ServiceShape service) { return model; } String ns = service.getId().getNamespace(); + Protocol protocol = ProtocolResolver.resolve(service, model); List updated = new ArrayList<>(); for (String requestName : List.of("TestInvokeMethodRequest", "TestInvokeAuthorizerRequest")) { model.getShape(ShapeId.fromParts(ns, requestName)) .flatMap(s -> s.asStructureShape()) .ifPresent(struct -> { StructureShape afterBody = TransformSupport - .renameMember(struct, "body", "requestBody").orElse(struct); - TransformSupport.renameMember(afterBody, "headers", "requestHeaders") + .renameMember(struct, "body", "requestBody", protocol).orElse(struct); + TransformSupport.renameMember(afterBody, "headers", "requestHeaders", protocol) .ifPresentOrElse(updated::add, () -> { if (afterBody != struct) { updated.add(afterBody); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java index afbb391a458..a1636497b07 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java @@ -6,6 +6,8 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; @@ -31,11 +33,12 @@ private static Model apply(Model model, ServiceShape service) { return model; } String ns = service.getId().getNamespace(); + Protocol protocol = ProtocolResolver.resolve(service, model); List updated = new ArrayList<>(); for (String requestName : List.of("ImportApiRequest", "ReimportApiRequest")) { model.getShape(ShapeId.fromParts(ns, requestName)) .flatMap(s -> s.asStructureShape()) - .flatMap(struct -> TransformSupport.renameMember(struct, "Body", "requestBody")) + .flatMap(struct -> TransformSupport.renameMember(struct, "Body", "requestBody", protocol)) .ifPresent(updated::add); } if (updated.isEmpty()) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java index 29879827cf7..19f27ff1965 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java @@ -71,8 +71,9 @@ private GlobalTransforms() {} * requestBody}, {@code headers -> headerValues}, {@code Headers -> headerValues}, honoring the * per-service skip-lists. Mirrors the legacy C2J {@code RESERVED_REQUEST_MEMBER_MAPPING}. Only * operation-input shapes are touched (never arbitrary domain shapes that happen to end in - * "Request"). A collision (the target member name already present) throws via - * {@link TransformSupport#renameMember}. + * "Request"). {@link TransformSupport#renameMember} preserves each renamed member's wire name + * via the service's protocol-appropriate trait (matching C2J's {@code setLocationName}), and + * throws on a collision (the target member name already present). * * @param model the current model * @param service the service being generated (its raw smithy name drives the skip-lists) @@ -80,6 +81,7 @@ private GlobalTransforms() {} */ static Model renameReservedRequestMembers(Model model, ServiceShape service) { String smithyServiceName = ServiceNameUtil.getSmithyServiceName(service, null); + Protocol protocol = ProtocolResolver.resolve(service, model); Set inputIds = new HashSet<>(); for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { inputIds.add(op.getInputShape()); @@ -91,7 +93,7 @@ static Model renameReservedRequestMembers(Model model, ServiceShape service) { boolean changed = false; for (Map.Entry rename : reservedRenames(current, smithyServiceName)) { Optional next = - TransformSupport.renameMember(current, rename.getKey(), rename.getValue()); + TransformSupport.renameMember(current, rename.getKey(), rename.getValue(), protocol); if (next.isPresent()) { current = next.get(); changed = true; @@ -248,9 +250,11 @@ public static Model injectResponseMetadata(Model model, ServiceShape service) { model.getShape(outputId).flatMap(Shape::asStructureShape).ifPresent(result -> { if (result.getMember(RESPONSE_METADATA).isPresent()) { throw new IllegalStateException("Result shape " + result.getId() - + " already has a '" + RESPONSE_METADATA + "' member; cannot inject the " - + "framework " + RESPONSE_METADATA + " envelope. Rename the modeled member " - + "via a per-service transform first."); + + " already has a '" + RESPONSE_METADATA + "' member, which collides with the " + + "framework " + RESPONSE_METADATA + " envelope. Resolve the collision in the " + + "raw model, or rename the modeled member earlier within GlobalTransforms: " + + "per-service transforms run after GlobalTransforms and cannot pre-empt this " + + "injection."); } StructureShape withMetadata = result.toBuilder() .addMember(MemberShape.builder() diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java index adac53b7a71..c844dc49107 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java @@ -61,20 +61,18 @@ private static Model apply(Model model, ServiceShape service) { List updated = new ArrayList<>(); for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { - if (!operations.contains(op.getId().getName())) { - continue; + if (operations.contains(op.getId().getName())) { + model.getShape(op.getInputShape()).flatMap(s -> s.asStructureShape()).ifPresent(req -> { + if (req.getMember(SOURCE_REGION).isEmpty()) { + updated.add(req.toBuilder() + .addMember(MemberShape.builder() + .id(req.getId().withMember(SOURCE_REGION)) + .target(ShapeId.from("smithy.api#String")) + .build()) + .build()); + } + }); } - model.getShape(op.getInputShape()).flatMap(s -> s.asStructureShape()).ifPresent(req -> { - if (req.getMember(SOURCE_REGION).isPresent()) { - return; - } - updated.add(req.toBuilder() - .addMember(MemberShape.builder() - .id(req.getId().withMember(SOURCE_REGION)) - .target(ShapeId.from("smithy.api#String")) - .build()) - .build()); - }); } if (updated.isEmpty()) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java index dd1d5aa35aa..67e31b6a511 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java @@ -5,13 +5,19 @@ package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.EnumRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait; import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.JsonNameTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.XmlNameTrait; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -83,28 +89,29 @@ static Optional appendValues(Shape enumShape, List values) { * preserving member declaration order and copying all traits onto the renamed member. Returns * {@link Optional#empty()} if {@code oldName} is absent (nothing to rename). * + *

The renamed member keeps its original wire name. A member with no explicit wire-name trait + * serializes under its member name, so renaming it would silently change the wire key; to + * prevent that this method pins the original name via the protocol-appropriate trait(s) + * ({@link #wireNamePreservingTraits}). This mirrors the legacy C2J rename primitive, which + * couples {@code setLocationName(originalMemberKey)} into the same step that changes the member + * key so a rename can never drop the wire name. + * * @throws IllegalStateException if {@code newName} is already a distinct member — a genuine - * collision that would silently drop a member. Callers must not mask this. - */ - static Optional renameMember(StructureShape struct, String oldName, String newName) { - return renameMember(struct, oldName, newName, new software.amazon.smithy.model.traits.Trait[0]); - } - - /** - * As {@link #renameMember(StructureShape, String, String)}, additionally attaching - * {@code extraTraits} to the renamed member (e.g. a {@code @jsonName} to preserve the original - * wire name when the C++ member name changes). Existing member traits are copied first, then the - * extras are added. + * collision that would silently drop a member; or if the protocol has no wire-name trait + * to preserve the original key (see {@link #wireNamePreservingTrait}). Callers must not + * mask either. */ static Optional renameMember(StructureShape struct, String oldName, String newName, - software.amazon.smithy.model.traits.Trait... extraTraits) { - if (struct.getMember(oldName).isEmpty()) { + Protocol protocol) { + Optional target = struct.getMember(oldName); + if (target.isEmpty()) { return Optional.empty(); } if (struct.getMember(newName).isPresent()) { throw new IllegalStateException("Cannot rename member '" + oldName + "' to '" + newName + "' on " + struct.getId() + ": a distinct '" + newName + "' member already exists"); } + List wireNameTraits = wireNamePreservingTraits(target.get(), oldName, protocol); StructureShape.Builder builder = StructureShape.builder().id(struct.getId()); struct.getAllTraits().values().forEach(builder::addTrait); for (MemberShape member : struct.getAllMembers().values()) { @@ -113,12 +120,69 @@ static Optional renameMember(StructureShape struct, String oldNa builder.addMember(name, member.getTarget(), b -> { member.getAllTraits().values().forEach(b::addTrait); if (isTarget) { - for (software.amazon.smithy.model.traits.Trait t : extraTraits) { - b.addTrait(t); - } + wireNameTraits.forEach(b::addTrait); } }); } return Optional.of(builder.build()); } + + /** + * The trait(s) to add to the renamed member so its wire name(s) stay equal to what {@code oldName} + * produced. Existing wire-name traits are always copied verbatim by the rename, so this only + * synthesizes what the member lacks; if a protocol's trait is already present, nothing is added + * for it. + * + *

    + *
  • JSON-family ({@code awsJson}, {@code restJson1}): {@code @jsonName}.
  • + *
  • {@code restXml} / {@code awsQuery}: {@code @xmlName}.
  • + *
  • {@code ec2Query}: request and response use different names, so both are pinned. + * The request query key is authoritative from {@code @ec2QueryName} (used verbatim); the + * response XML element is {@code @xmlName}. EC2 models routinely carry an {@code @xmlName} + * that is not merely the camelCase of the request key (e.g. member {@code Ipv6Addresses} + * has {@code ec2QueryName=Ipv6Addresses} but {@code xmlName=ipv6AddressesSet}), so + * reconstructing the request key from {@code capitalize(@xmlName)} — what legacy C2J does — + * is unreliable. We instead pin {@code @ec2QueryName} to the member's current request key + * ({@code capitalize(@xmlName ?? memberName)} when it has none of its own) so it survives + * the member-name change without depending on any serde-time fallback.
  • + *
  • Any other protocol (e.g. {@code rpcv2Cbor}, which has no wire-name trait and always + * serializes under the member name): fail fast rather than emit an inert trait and + * mis-generate later.
  • + *
+ */ + private static List wireNamePreservingTraits(MemberShape member, String oldName, + Protocol protocol) { + if (protocol == Protocol.EC2) { + List traits = new ArrayList<>(); + if (!member.hasTrait(Ec2QueryNameTrait.class)) { + String responseName = member.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue).orElse(oldName); + traits.add(new Ec2QueryNameTrait(capitalizeFirst(responseName))); + } + if (!member.hasTrait(XmlNameTrait.class)) { + traits.add(new XmlNameTrait(oldName)); + } + return traits; + } + if (protocol.isXmlLike()) { + return member.hasTrait(XmlNameTrait.class) + ? List.of() : List.of(new XmlNameTrait(oldName)); + } + if (protocol.isJsonLike()) { + return member.hasTrait(JsonNameTrait.class) + ? List.of() : List.of(new JsonNameTrait(oldName)); + } + throw new IllegalStateException("Cannot preserve the wire name of renamed member '" + oldName + + "' under protocol " + protocol + ": it has no wire-name trait (rpcv2Cbor always " + + "serializes under the member name), so the rename would silently change the wire key. " + + "Add explicit wire-name handling for this protocol before renaming its members."); + } + + /** Uppercases the first character; the EC2 query-key casing rule. */ + private static String capitalizeFirst(String value) { + if (value.isEmpty()) { + return value; + } + return Character.toUpperCase(value.charAt(0)) + value.substring(1); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index a1c388cda2b..727706c24aa 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.JsonNameTrait; +import software.amazon.smithy.model.traits.XmlNameTrait; import java.util.Set; @@ -104,6 +106,67 @@ void reservedRename_collision_throws() { () -> GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example"))); } + @Test + void reservedRename_jsonService_preservesWireNameWithJsonName() { + Model m = inputModel("Kinesis", "body"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + MemberShape renamed = input(out).getMember("requestBody").orElseThrow(); + assertEquals("body", renamed.expectTrait(JsonNameTrait.class).getValue(), + "JSON service must keep the 'body' wire key via @jsonName"); + assertFalse(renamed.hasTrait(XmlNameTrait.class)); + } + + @Test + void reservedRename_queryXmlService_preservesWireNameWithXmlName() { + Model m = inputModelWithProtocol("Kinesis", + new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait(), "body"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + MemberShape renamed = input(out).getMember("requestBody").orElseThrow(); + assertEquals("body", renamed.expectTrait(XmlNameTrait.class).getValue(), + "awsQuery service must keep the 'body' wire key via @xmlName"); + assertFalse(renamed.hasTrait(JsonNameTrait.class)); + assertFalse(renamed.hasTrait( + software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait.class), + "awsQuery must not use the ec2Query request-key trait"); + } + + @Test + void reservedRename_ec2Service_pinsRequestKeyAndResponseName() { + // ec2Query request key (@ec2QueryName, capitalized, verbatim on the wire) and response XML + // name (@xmlName) differ, so both are pinned rather than relying on capitalize(@xmlName). + Model m = inputModelWithProtocol("Kinesis", + new software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait(), "body"); + Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + MemberShape renamed = input(out).getMember("requestBody").orElseThrow(); + assertEquals("Body", renamed.expectTrait( + software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait.class).getValue(), + "ec2Query request key must be preserved verbatim as the capitalized original member name"); + assertEquals("body", renamed.expectTrait(XmlNameTrait.class).getValue(), + "ec2Query response XML name must be preserved as the original member name"); + assertFalse(renamed.hasTrait(JsonNameTrait.class)); + } + + /** As {@link #inputModel} but with the given protocol trait(s) on the service. */ + private static Model inputModelWithProtocol(String sdkId, + software.amazon.smithy.model.traits.Trait protocolTrait, + String... inputMembers) { + StructureShape.Builder in = StructureShape.builder().id("com.example#DoThingRequest"); + for (String member : inputMembers) { + in.addMember(member, ShapeId.from("smithy.api#String")); + } + StructureShape input = in.build(); + StructureShape output = StructureShape.builder().id("com.example#DoThingResponse").build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoThing").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(software.amazon.smithy.aws.traits.ServiceTrait.builder() + .sdkId(sdkId).arnNamespace("x").cloudFormationName("X").cloudTrailEventSource("x").build()) + .addTrait(protocolTrait) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(input, output, op, service).assemble().unwrap(); + } + // --- computeReachableShapes tests --- @Test diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java index ddacd6fe035..cc70ad9c1c6 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java @@ -8,6 +8,7 @@ import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.JsonNameTrait; import static org.junit.jupiter.api.Assertions.*; @@ -59,6 +60,10 @@ void renamesBodyAndHeaders() { assertTrue(r.getMember("requestHeaders").isPresent()); assertTrue(r.getMember("body").isEmpty()); assertTrue(r.getMember("headers").isEmpty()); + assertEquals("body", r.getMember("requestBody").orElseThrow() + .expectTrait(JsonNameTrait.class).getValue(), "body wire key preserved"); + assertEquals("headers", r.getMember("requestHeaders").orElseThrow() + .expectTrait(JsonNameTrait.class).getValue(), "headers wire key preserved"); StructureShape a = out.expectShape( ShapeId.from("com.example#TestInvokeAuthorizerRequest"), StructureShape.class); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java index 82dcb27db15..9ac34b20a9e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java @@ -8,6 +8,7 @@ import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.JsonNameTrait; import static org.junit.jupiter.api.Assertions.*; @@ -51,6 +52,8 @@ void renamesBody() { StructureShape r = out.expectShape(ShapeId.from("com.example#" + name), StructureShape.class); assertTrue(r.getMember("requestBody").isPresent(), name); assertTrue(r.getMember("Body").isEmpty(), name); + assertEquals("Body", r.getMember("requestBody").orElseThrow() + .expectTrait(JsonNameTrait.class).getValue(), name + " wire key preserved"); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java index 1bc07bf6656..e5d23270cb4 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java @@ -4,15 +4,17 @@ */ package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.JsonNameTrait; +import software.amazon.smithy.model.traits.XmlNameTrait; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -28,20 +30,22 @@ private static StructureShape struct(String... memberNames) { @Test void renameMember_sourceAbsent_returnsEmpty() { - assertTrue(TransformSupport.renameMember(struct("name"), "body", "requestBody").isEmpty()); + assertTrue(TransformSupport.renameMember(struct("name"), "body", "requestBody", Protocol.JSON) + .isEmpty()); } @Test void renameMember_targetExists_throws() { StructureShape s = struct("body", "requestBody"); assertThrows(IllegalStateException.class, - () -> TransformSupport.renameMember(s, "body", "requestBody")); + () -> TransformSupport.renameMember(s, "body", "requestBody", Protocol.JSON)); } @Test void renameMember_success_renamesAndPreservesOrder() { StructureShape s = struct("a", "body", "z"); - StructureShape out = TransformSupport.renameMember(s, "body", "requestBody").orElseThrow(); + StructureShape out = TransformSupport.renameMember(s, "body", "requestBody", Protocol.JSON) + .orElseThrow(); assertFalse(out.getMember("body").isPresent()); assertTrue(out.getMember("requestBody").isPresent()); List order = new ArrayList<>(out.getAllMembers().keySet()); @@ -49,12 +53,86 @@ void renameMember_success_renamesAndPreservesOrder() { } @Test - void renameMember_withJsonName_attachesTraitToRenamedMember() { - StructureShape s = struct("generatedPolicyResult"); - StructureShape out = TransformSupport.renameMember( - s, "generatedPolicyResult", "generatedPolicyResults", - new JsonNameTrait("generatedPolicyResult")).orElseThrow(); - MemberShape renamed = out.getMember("generatedPolicyResults").orElseThrow(); - assertEquals("generatedPolicyResult", renamed.expectTrait(JsonNameTrait.class).getValue()); + void renameMember_jsonProtocol_pinsWireNameWithJsonName() { + StructureShape out = TransformSupport.renameMember(struct("body"), "body", "requestBody", + Protocol.REST_JSON).orElseThrow(); + MemberShape renamed = out.getMember("requestBody").orElseThrow(); + assertEquals("body", renamed.expectTrait(JsonNameTrait.class).getValue()); + assertFalse(renamed.hasTrait(XmlNameTrait.class), "JSON protocol must not add @xmlName"); + } + + @Test + void renameMember_xmlProtocol_pinsWireNameWithXmlName() { + StructureShape out = TransformSupport.renameMember(struct("body"), "body", "requestBody", + Protocol.QUERY_XML).orElseThrow(); + MemberShape renamed = out.getMember("requestBody").orElseThrow(); + assertEquals("body", renamed.expectTrait(XmlNameTrait.class).getValue()); + assertFalse(renamed.hasTrait(JsonNameTrait.class), "XML protocol must not add @jsonName"); + } + + @Test + void renameMember_ec2Protocol_bareMember_pinsRequestKeyAndResponseName() { + StructureShape out = TransformSupport.renameMember(struct("body"), "body", "requestBody", + Protocol.EC2).orElseThrow(); + MemberShape renamed = out.getMember("requestBody").orElseThrow(); + assertEquals("Body", renamed.expectTrait(Ec2QueryNameTrait.class).getValue(), + "request key = capitalized original member name, verbatim"); + assertEquals("body", renamed.expectTrait(XmlNameTrait.class).getValue(), + "response name = original member name"); + } + + @Test + void renameMember_ec2Protocol_derivesRequestKeyFromExistingXmlName() { + // CapacityReservationFleetIds: @xmlName present, no @ec2QueryName. The request key is + // capitalize(@xmlName), pinned verbatim so it no longer depends on the member name. + StructureShape s = StructureShape.builder().id("com.example#Req") + .addMember(MemberShape.builder().id("com.example#Req$capacityReservationFleetIds") + .target("smithy.api#String") + .addTrait(new XmlNameTrait("CapacityReservationFleetId")).build()) + .build(); + MemberShape renamed = TransformSupport.renameMember(s, "capacityReservationFleetIds", + "renamed", Protocol.EC2).orElseThrow().getMember("renamed").orElseThrow(); + assertEquals("CapacityReservationFleetId", + renamed.expectTrait(Ec2QueryNameTrait.class).getValue(), "request key from capitalize(@xmlName)"); + assertEquals("CapacityReservationFleetId", + renamed.expectTrait(XmlNameTrait.class).getValue(), "existing @xmlName preserved verbatim"); + } + + @Test + void renameMember_ec2Protocol_existingEc2QueryName_isNotOverridden() { + // Ipv6Addresses: @ec2QueryName is NOT camelCase(@xmlName), so capitalize(@xmlName) would be + // wrong for the request. Both existing traits must ride along verbatim. + StructureShape s = StructureShape.builder().id("com.example#Req") + .addMember(MemberShape.builder().id("com.example#Req$ipv6Addresses") + .target("smithy.api#String") + .addTrait(new Ec2QueryNameTrait("Ipv6Addresses")) + .addTrait(new XmlNameTrait("ipv6AddressesSet")).build()) + .build(); + MemberShape renamed = TransformSupport.renameMember(s, "ipv6Addresses", "renamed", + Protocol.EC2).orElseThrow().getMember("renamed").orElseThrow(); + assertEquals("Ipv6Addresses", renamed.expectTrait(Ec2QueryNameTrait.class).getValue()); + assertEquals("ipv6AddressesSet", renamed.expectTrait(XmlNameTrait.class).getValue()); + } + + @Test + void renameMember_cborProtocol_throws_noWireNameTrait() { + // rpcv2Cbor has no wire-name trait and ignores @jsonName, so a rename cannot preserve the + // wire key — fail fast rather than silently mis-generate. + StructureShape s = struct("body"); + assertThrows(IllegalStateException.class, + () -> TransformSupport.renameMember(s, "body", "requestBody", Protocol.CBOR)); + } + + @Test + void renameMember_existingJsonName_isNotOverridden() { + StructureShape s = StructureShape.builder().id("com.example#Req") + .addMember(MemberShape.builder().id("com.example#Req$body") + .target("smithy.api#String").addTrait(new JsonNameTrait("wireBody")).build()) + .build(); + StructureShape out = TransformSupport.renameMember(s, "body", "requestBody", Protocol.JSON) + .orElseThrow(); + MemberShape renamed = out.getMember("requestBody").orElseThrow(); + assertEquals("wireBody", renamed.expectTrait(JsonNameTrait.class).getValue(), + "existing wire name must be preserved verbatim, not reset to the old member name"); } } From e1a2763a5e083040c0217d5e456e75c83525baa6 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 27 Aug 2026 11:03:39 -0400 Subject: [PATCH 10/44] Smithy: add static DynamoDB AttributeValue resource bodies --- .../model/dynamodb/AttributeValue.cpp | 292 ++++++++++++++++++ .../model/dynamodb/AttributeValue.h | 166 ++++++++++ .../model/dynamodb/AttributeValueValue.cpp | 275 +++++++++++++++++ .../model/dynamodb/AttributeValueValue.h | 221 +++++++++++++ 4 files changed, 954 insertions(+) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.cpp create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.h create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.cpp create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.h diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.cpp b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.cpp new file mode 100644 index 00000000000..174eb686901 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.cpp @@ -0,0 +1,292 @@ +#include +#include + +#include + +using namespace Aws::DynamoDB::Model; +using namespace Aws::Utils; +using namespace Aws::Utils::Json; + +const Aws::String AttributeValue::GetS() const { + if (m_value) { + return m_value->GetS(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetS(const Aws::String& s) { + m_value = Aws::MakeShared("AttributeValue", s); + return *this; +} + +const Aws::String AttributeValue::GetN() const { + if (m_value) { + return m_value->GetN(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetN(const Aws::String& n) { + m_value = Aws::MakeShared("AttributeValue", n); + return *this; +} + +const ByteBuffer AttributeValue::GetB() const { + if (m_value) { + return m_value->GetB(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetB(const ByteBuffer& b) { + m_value = Aws::MakeShared("AttributeValue", b); + return *this; +} + +const ByteBuffer& AttributeValue::AccessB() const { + if (m_value) { + return m_value->AccessB(); + } else { + static const ByteBuffer empty; + return empty; + } +} + +const Aws::Vector AttributeValue::GetSS() const { + if (m_value) { + return m_value->GetSS(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetSS(const Aws::Vector& ss) { + m_value = Aws::MakeShared("AttributeValue", ss); + return *this; +} + +AttributeValue& AttributeValue::AddSItem(const Aws::String& sItem) { + if (!m_value) { + Aws::Vector ss; + ss.push_back(sItem); + m_value = Aws::MakeShared("AttributeValue", ss); + } else { + m_value->AddSItem(sItem); + } + return *this; +} + +const Aws::Vector AttributeValue::GetNS() const { + if (m_value) { + return m_value->GetNS(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetNS(const Aws::Vector& ns) { + m_value = Aws::MakeShared("AttributeValue", ns); + return *this; +} + +AttributeValue& AttributeValue::AddNItem(const Aws::String& nItem) { + if (!m_value) { + Aws::Vector ns; + ns.push_back(nItem); + m_value = Aws::MakeShared("AttributeValue", ns); + } else { + m_value->AddNItem(nItem); + } + return *this; +} + +const Aws::Vector AttributeValue::GetBS() const { + if (m_value) { + return m_value->GetBS(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetBS(const Aws::Vector& bs) { + m_value = Aws::MakeShared("AttributeValue", bs); + return *this; +} + +AttributeValue& AttributeValue::AddBItem(const ByteBuffer& bItem) { + if (!m_value) { + Aws::Vector bs; + bs.push_back(bItem); + m_value = Aws::MakeShared("AttributeValue", bs); + } else { + m_value->AddBItem(bItem); + } + return *this; +} + +AttributeValue& AttributeValue::AddBItem(const unsigned char* bItem, size_t size) { return AddBItem(ByteBuffer(bItem, size)); } + +const Aws::Map> AttributeValue::GetM() const { + if (m_value) { + return m_value->GetM(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetM(const Aws::Map>& map) { + m_value = Aws::MakeShared("AttributeValue", map); + return *this; +} + +AttributeValue& AttributeValue::AddMEntry(const Aws::String& key, const std::shared_ptr& value) { + if (!m_value) { + Aws::Map> map; + auto kvp = std::pair>(key, value); + map.insert(map.begin(), kvp); + m_value = Aws::MakeShared("AttributeValue", map); + } else { + m_value->AddMEntry(key, value); + } + + return *this; +} + +const Aws::Vector> AttributeValue::GetL() const { + if (m_value) { + return m_value->GetL(); + } else { + return {}; + } +} + +AttributeValue& AttributeValue::SetL(const Aws::Vector>& list) { + m_value = Aws::MakeShared("AttributeValue", list); + return *this; +} + +AttributeValue& AttributeValue::AddLItem(const std::shared_ptr& listItem) { + if (!m_value) { + Aws::Vector> list; + list.push_back(listItem); + m_value = Aws::MakeShared("AttributeValue", list); + } else { + m_value->AddLItem(listItem); + } + + return *this; +} + +bool AttributeValue::GetBool() const { + if (m_value) { + return m_value->GetBool(); + } else { + return false; + } +} + +AttributeValue& AttributeValue::SetBool(bool value) { + m_value = Aws::MakeShared("AttributeValue", value); + return *this; +} + +bool AttributeValue::GetNull() const { + if (m_value) { + return m_value->GetNull(); + } else { + return false; + } +} + +AttributeValue& AttributeValue::SetNull(bool value) { + m_value = Aws::MakeShared("AttributeValue", value); + return *this; +} + +AttributeValue& AttributeValue::operator=(JsonView jsonValue) { + if (jsonValue.ValueExists("S")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("N")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("B")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("SS")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("NS")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("BS")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("M")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("L")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("BOOL")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + if (jsonValue.ValueExists("NULL")) { + m_value = Aws::MakeShared("AttributeValue", jsonValue); + return *this; + } + + return *this; +} + +bool AttributeValue::operator==(const AttributeValue& other) const { + if (this == &other) return true; + + if (m_value) { + if (other.m_value) { + return *m_value == *other.m_value; + } else { + return m_value->IsDefault(); + } + } else if (other.m_value) { + return other.m_value->IsDefault(); + } + + return true; +} + +JsonValue AttributeValue::Jsonize() const { + if (m_value) { + return m_value->Jsonize(); + } else { + return JsonValue(); + } +} + +Aws::String AttributeValue::SerializeAttribute() const { + JsonValue value = Jsonize(); + return value.View().WriteReadable(); +} + +Aws::DynamoDB::Model::ValueType AttributeValue::GetType() const { return m_value->GetType(); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.h b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.h new file mode 100644 index 00000000000..299b8e21f87 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValue.h @@ -0,0 +1,166 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Aws { +namespace DynamoDB { +namespace Model { +class AttributeValueValue; + +enum class ValueType { STRING, NUMBER, BYTEBUFFER, STRING_SET, NUMBER_SET, BYTEBUFFER_SET, ATTRIBUTE_MAP, ATTRIBUTE_LIST, BOOL, NULLVALUE }; + +/// http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html +class AWS_DYNAMODB_API AttributeValue { + public: + AttributeValue() {}; + explicit AttributeValue(const Aws::String& s) { SetS(s); } + explicit AttributeValue(const Aws::Vector& ss) { SetSS(ss); } + AttributeValue(Aws::Utils::Json::JsonView jsonValue) { *this = jsonValue; } + + /// returns the String value if the value is specialized to this type, otherwise an empty String + const Aws::String GetS() const; + /// if already specialized to a String, sets the value to this String + /// if uninitialized, specializes the type to a String with specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetS(const Aws::String& s); + /// if uninitialized, specializes the type to a String with specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetS(const char* n) { return SetS(Aws::String(n)); } + + /// returns the Number value if the value is specialized to this type, otherwise an empty String + const Aws::String GetN() const; + /// if already specialized to a Number, sets the value to this Number + /// if uninitialized, specializes the type to a Number with specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetN(const Aws::String& n); + /// if already specialized to a Number, sets the value to this Number + /// if uninitialized, specializes the type to a Number with specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetN(const char* n) { return SetN(Aws::String(n)); } + /// if already specialized to a Number, sets the value to this Number + /// if uninitialized, specializes the type to a Number with specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetN(const int nItem) { return SetN(Aws::String(std::to_string(nItem).c_str())); } + /// if already specialized to a Number, sets the value to this Number + /// if uninitialized, specializes the type to a Number with specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetN(const float nItem) { return SetN(Aws::String(std::to_string(nItem).c_str())); } + /// if already specialized to a Number, sets the value to this Number + /// if uninitialized, specializes the type to a Number with specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetN(const double nItem) { return SetN(Aws::String(std::to_string(nItem).c_str())); } + + /// returns the ByteBuffer if the value is specialized to this type, otherwise an empty Buffer + const Aws::Utils::ByteBuffer GetB() const; + /// returns a reference to the ByteBuffer if the value is specialized to this type, otherwise a reference to an empty Buffer + const Aws::Utils::ByteBuffer& AccessB() const; + /// if already specialized to a ByteBuffer, sets the value to this value + /// if uninitialized, specializes the type to a ByteBuffer with the specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetB(const Aws::Utils::ByteBuffer& b); + + /// returns the String Vector if the value is specialized to this type, otherwise an empty Vector + const Aws::Vector GetSS() const; + /// if already specialized to a String Set, sets to these values + /// if uninitialized, specializes the type to a String Set with specified values + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetSS(const Aws::Vector& ss); + /// if the value is already specialized to a String Set then this value is appended + /// if uninitialized, specializes the type to a String Set with this initial value + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddSItem(const Aws::String& sItem); + /// if the value is already specialized to a String Set then this value is appended + /// if uninitialized, specializes the type to a String Set with this initial value + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddSItem(const char* sItem) { return AddSItem(Aws::String(sItem)); } + + /// returns the Number Vector if the value is specialized to this type, otherwise an empty Vector + const Aws::Vector GetNS() const; + /// if already specialized to a Number Set, sets to these values + /// if uninitialized, specializes the type to a Number Set with specified values + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetNS(const Aws::Vector& ns); + /// if the value is already specialized to a Number Set then this value is appended + /// if uninitialized, specializes the type to a Number Set with this initial value + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddNItem(const Aws::String& nItem); + /// if the value is already specialized to a Number Set then this value is appended + /// if uninitialized, specializes the type to a Number Set with this initial value + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddNItem(const char* nItem) { return AddNItem(Aws::String(nItem)); } + + /// returns the ByteBuffer Vector if the value is specialized to this type, otherwise an empty Vector + const Aws::Vector GetBS() const; + /// if already specialized to a ByteBuffer Set, sets to these values + /// if uninitialized, specializes the type to a ByteBuffer Set with specified values + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetBS(const Aws::Vector& bs); + /// if the value is already specialized to a ByteBuffer Set then this value is appended + /// if uninitialized, specializes the type to a ByteBuffer Set with this initial value + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddBItem(const Aws::Utils::ByteBuffer& bItem); + /// if the value is already specialized to a ByteBuffer Set then this value is appended + /// if uninitialized, specializes the type to a ByteBuffer Set with this initial value + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddBItem(const unsigned char* bItem, size_t size); + + /// returns the Attribute Map if the value is specialized to this type, otherwise an empty Map + const Aws::Map> GetM() const; + /// if already specialized to an Attribute Map, sets to these values + /// if uninitialized, specializes the type to an Attribute Map with specified values + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetM(const Aws::Map>& map); + /// if the value is already specialized to a Map then this value is inserted + /// if uninitialized, specializes the type to a Map with these initial values + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddMEntry(const Aws::String& key, const std::shared_ptr& value); + /// if the value is already specialized to a Map then this value is inserted + /// if uninitialized, specializes the type to a Map with these initial values + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddMEntry(const char* key, const std::shared_ptr& value) { return AddMEntry(Aws::String(key), value); } + + /// returns the Attribute List if the value is specialized to this type, otherwise an empty Vector + const Aws::Vector> GetL() const; + /// if already specialized to an Attribute List, sets to these values + /// if uninitialized, specializes the type to an Attribute List with specified values + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetL(const Aws::Vector>& list); + /// if the value is already specialized to a List then this value is appended + /// if uninitialized, specializes the type to a List with these initial values + /// if already specialized to another type then the behavior is undefined + AttributeValue& AddLItem(const std::shared_ptr& listItem); + + /// returns the boolean if the value is specialized to this type, otherwise false + bool GetBool() const; + /// if already specialized to a boolean, sets to this value + /// if uninitialized, specializes the type to a boolean with the specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetBool(bool value); + + /// returns Null-Set-Value if the value is specialized to this type, otherwise false + bool GetNull() const; + /// if already specialized to a Null, sets to this value + /// if uninitialized, specializes the type to Null with the specified value + /// if already specialized to another type then the behavior is undefined + AttributeValue& SetNull(bool value); + + AttributeValue& operator=(Aws::Utils::Json::JsonView); + + bool operator==(const AttributeValue& other) const; + inline bool operator!=(const AttributeValue& other) const { return !(*this == other); } + + Aws::String SerializeAttribute() const; + Aws::Utils::Json::JsonValue Jsonize() const; + ValueType GetType() const; + + private: + std::shared_ptr m_value; +}; + +} // namespace Model +} // namespace DynamoDB +} // namespace Aws diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.cpp b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.cpp new file mode 100644 index 00000000000..1ed20e24d26 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.cpp @@ -0,0 +1,275 @@ +#include +#include + +using namespace Aws::DynamoDB::Model; +using namespace Aws::Utils; +using namespace Aws::Utils::Json; +using Aws::Utils::Array; + +// +// Strings +// + +JsonValue AttributeValueString::Jsonize() const { + JsonValue value; + + value.WithString("S", m_s); + + return value; +} + +// +// Numerics +// + +JsonValue AttributeValueNumeric::Jsonize() const { + JsonValue value; + + if (!m_n.empty()) { + value.WithString("N", m_n); + } + + return value; +} + +// +// ByteBuffers +// + +AttributeValueByteBuffer::AttributeValueByteBuffer(JsonView jsonValue) { m_b = HashingUtils::Base64Decode(jsonValue.GetString("B")); } + +JsonValue AttributeValueByteBuffer::Jsonize() const { + JsonValue value; + + value.WithString("B", HashingUtils::Base64Encode(m_b)); + + return value; +} + +// +// String Sets +// + +AttributeValueStringSet::AttributeValueStringSet(JsonView jsonValue) { + Aws::Utils::Array ss = jsonValue.GetArray("SS"); + + for (unsigned i = 0; i < ss.GetLength(); ++i) { + m_sS.push_back(ss[i].AsString()); + } +} + +bool AttributeValueStringSet::operator==(const AttributeValueValue& other) const { + const Aws::Vector& other_sS(other.GetSS()); + + if (GetType() != other.GetType() || m_sS.size() != other_sS.size()) return false; + + for (unsigned i = 0; i < m_sS.size(); ++i) + if (m_sS[i] != other_sS[i]) return false; + + return true; +} + +JsonValue AttributeValueStringSet::Jsonize() const { + JsonValue value; + + if (m_sS.size() > 0) { + Aws::Utils::Array array(m_sS.size()); + for (unsigned i = 0; i < m_sS.size(); ++i) { + array[i].AsString(m_sS[i]); + } + value.WithArray("SS", std::move(array)); + } + + return value; +} + +// +// Number Sets +// + +AttributeValueNumberSet::AttributeValueNumberSet(JsonView jsonValue) { + const Aws::Utils::Array ns = jsonValue.GetArray("NS"); + + for (unsigned i = 0; i < ns.GetLength(); ++i) { + m_nS.push_back(ns[i].AsString()); + } +} + +bool AttributeValueNumberSet::operator==(const AttributeValueValue& other) const { + const Aws::Vector& other_nS(other.GetNS()); + + if (GetType() != other.GetType() || m_nS.size() != other_nS.size()) return false; + + for (unsigned i = 0; i < m_nS.size(); ++i) + if (m_nS[i] != other_nS[i]) return false; + + return true; +} + +JsonValue AttributeValueNumberSet::Jsonize() const { + JsonValue value; + + if (m_nS.size() > 0) { + Aws::Utils::Array array(m_nS.size()); + for (unsigned i = 0; i < m_nS.size(); ++i) { + array[i].AsString(m_nS[i]); + } + value.WithArray("NS", std::move(array)); + } + + return value; +} + +// +// ByteBuffer Sets +// + +AttributeValueByteBufferSet::AttributeValueByteBufferSet(JsonView jsonValue) { + const Aws::Utils::Array bs = jsonValue.GetArray("BS"); + + for (unsigned i = 0; i < bs.GetLength(); ++i) { + m_bS.push_back(HashingUtils::Base64Decode(bs[i].AsString())); + } +} + +bool AttributeValueByteBufferSet::operator==(const AttributeValueValue& other) const { + const Aws::Vector& other_bS(other.GetBS()); + + if (GetType() != other.GetType() || m_bS.size() != other_bS.size()) return false; + + for (unsigned i = 0; i < m_bS.size(); ++i) + if (m_bS[i] != other_bS[i]) return false; + + return true; +} + +JsonValue AttributeValueByteBufferSet::Jsonize() const { + JsonValue value; + + if (m_bS.size() > 0) { + Aws::Utils::Array array(m_bS.size()); + for (unsigned i = 0; i < m_bS.size(); ++i) { + array[i].AsString(HashingUtils::Base64Encode(m_bS[i])); + } + value.WithArray("BS", std::move(array)); + } + + return value; +} + +// +// AttributeValue Map +// + +AttributeValueMap::AttributeValueMap(JsonView jsonValue) { + const Aws::Map map = jsonValue.GetObject("M").GetAllObjects(); + + for (auto& item : map) { + std::shared_ptr attributeValue = Aws::MakeShared("AttributeValue"); + JsonView itemValue = item.second; + *attributeValue = itemValue; + + m_m.emplace(item.first, std::move(attributeValue)); + } +} + +void AttributeValueMap::AddMEntry(const Aws::String& key, const std::shared_ptr& value) { + m_m.insert(m_m.begin(), std::pair>(key, value)); +} + +bool AttributeValueMap::operator==(const AttributeValueValue& other) const { + const Aws::Map>& other_m(other.GetM()); + + if (GetType() != other.GetType() || m_m.size() != other_m.size()) return false; + + if (m_m.size() > 0) { + for (auto& mapItem : m_m) { + auto foundItem = other_m.find(mapItem.first); + if (foundItem == other_m.end()) return false; + + if (*foundItem->second != *mapItem.second) return false; + } + } + + return true; +} + +JsonValue AttributeValueMap::Jsonize() const { + JsonValue value; + + JsonValue mapValue; + for (auto& mapItem : m_m) { + JsonValue mapEntry = mapItem.second->Jsonize(); + mapValue.WithObject(mapItem.first, std::move(mapEntry)); + } + value.WithObject("M", std::move(mapValue)); + + return value; +} + +// +// AttributeValue List +// + +AttributeValueList::AttributeValueList(JsonView jsonValue) { + const Aws::Utils::Array array = jsonValue.GetArray("L"); + + for (unsigned i = 0; i < array.GetLength(); ++i) { + std::shared_ptr attributeValue = Aws::MakeShared("AttributeValue"); + JsonView itemValue = array[i]; + *attributeValue = itemValue; + m_l.push_back(attributeValue); + } +} + +bool AttributeValueList::operator==(const AttributeValueValue& other) const { + const Aws::Vector>& other_l(other.GetL()); + + if (GetType() != other.GetType() || m_l.size() != other_l.size()) return false; + + if (m_l.size() > 0) { + for (unsigned i = 0; i < m_l.size(); ++i) { + if (*m_l[i] != *other_l[i]) return false; + } + } + + return true; +} + +JsonValue AttributeValueList::Jsonize() const { + JsonValue value; + + Aws::Utils::Array list(m_l.size()); + + for (unsigned i = 0; i < m_l.size(); ++i) { + list[i] = m_l[i]->Jsonize(); + } + + value.WithArray("L", std::move(list)); + + return value; +} + +// +// Bool type +// + +JsonValue AttributeValueBool::Jsonize() const { + JsonValue value; + + value.WithBool("BOOL", m_bool); + + return value; +} + +// +// Null type +// + +JsonValue AttributeValueNull::Jsonize() const { + JsonValue value; + + value.WithBool("NULL", m_null); + + return value; +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.h b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.h new file mode 100644 index 00000000000..f0a92e47095 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/resources/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/AttributeValueValue.h @@ -0,0 +1,221 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace Aws { +namespace DynamoDB { +namespace Model { + +class AttributeValue; + +class AttributeValueValue { + public: + virtual const Aws::String GetS() const { return {}; } + + virtual const Aws::String GetN() const { return {}; } + + virtual const Aws::Utils::ByteBuffer GetB() const { return {}; } + + virtual const Aws::Utils::ByteBuffer& AccessB() const { + static const Aws::Utils::ByteBuffer empty; + return empty; + } + + virtual const Aws::Vector GetSS() const { return {}; } + + virtual void AddSItem(const Aws::String&) { assert(false); } + + virtual const Aws::Vector GetNS() const { return {}; } + + virtual void AddNItem(const Aws::String&) { assert(false); } + + virtual const Aws::Vector GetBS() const { return {}; } + + virtual void AddBItem(const Aws::Utils::ByteBuffer&) { assert(false); } + + virtual const Aws::Map> GetM() const { return {}; } + + virtual void AddMEntry(const Aws::String&, const std::shared_ptr&) { assert(false); } + + virtual const Aws::Vector> GetL() const { return {}; } + + virtual void AddLItem(const std::shared_ptr&) { assert(false); } + + virtual bool GetBool() const { return false; } + + virtual bool GetNull() const { return false; } + + virtual bool IsDefault() const = 0; + + virtual bool operator==(const AttributeValueValue& other) const = 0; + + virtual Aws::Utils::Json::JsonValue Jsonize() const = 0; + + virtual ValueType GetType() const = 0; +}; + +/// String data type +class AttributeValueString final : public AttributeValueValue { + public: + explicit AttributeValueString(const Aws::String& value) : m_s(value) {} + explicit AttributeValueString(Aws::Utils::Json::JsonView jsonValue) : m_s(jsonValue.GetString("S")) {} + const Aws::String GetS() const override { return m_s; } + bool IsDefault() const override { return m_s.empty(); } + bool operator==(const AttributeValueValue& other) const override { return GetType() == other.GetType() && m_s == other.GetS(); } + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::STRING; } + + private: + Aws::String m_s; +}; + +/// Numeric data type +class AttributeValueNumeric final : public AttributeValueValue { + public: + explicit AttributeValueNumeric(const Aws::String& value) : m_n(value) {} + explicit AttributeValueNumeric(Aws::Utils::Json::JsonView jsonValue) : m_n(jsonValue.GetString("N")) {} + const Aws::String GetN() const override { return m_n; } + bool IsDefault() const override { return m_n.empty(); } + bool operator==(const AttributeValueValue& other) const override { return GetType() == other.GetType() && m_n == other.GetN(); }; + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::NUMBER; } + + private: + Aws::String m_n; +}; + +/// Binary data type +class AttributeValueByteBuffer final : public AttributeValueValue { + public: + explicit AttributeValueByteBuffer(const Aws::Utils::ByteBuffer& value) : m_b(value) {} + explicit AttributeValueByteBuffer(Aws::Utils::Json::JsonView jsonValue); + const Aws::Utils::ByteBuffer GetB() const override { return m_b; } + const Aws::Utils::ByteBuffer& AccessB() const override { return m_b; } + bool IsDefault() const override { return m_b.GetLength() == 0; } + bool operator==(const AttributeValueValue& other) const override { return GetType() == other.GetType() && m_b == other.GetB(); } + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::BYTEBUFFER; } + + private: + Aws::Utils::ByteBuffer m_b; +}; + +/// String set data type +class AttributeValueStringSet final : public AttributeValueValue { + public: + explicit AttributeValueStringSet(const Aws::Vector& value) : m_sS(value) {} + explicit AttributeValueStringSet(Aws::Utils::Json::JsonView jsonValue); + const Aws::Vector GetSS() const override { return m_sS; } + void AddSItem(const Aws::String& sItem) override { m_sS.push_back(sItem); } + bool IsDefault() const override { return m_sS.empty(); } + bool operator==(const AttributeValueValue& other) const override; + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::STRING_SET; } + + private: + Aws::Vector m_sS; +}; + +/// Number set data type +class AttributeValueNumberSet final : public AttributeValueValue { + public: + explicit AttributeValueNumberSet(const Aws::Vector& value) : m_nS(value) {} + explicit AttributeValueNumberSet(Aws::Utils::Json::JsonView jsonValue); + const Aws::Vector GetNS() const override { return m_nS; } + void AddNItem(const Aws::String& nItem) override { m_nS.push_back(nItem); } + bool IsDefault() const override { return m_nS.empty(); } + bool operator==(const AttributeValueValue& other) const override; + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::NUMBER_SET; } + + private: + Aws::Vector m_nS; +}; + +/// ByteByffer set data type +class AttributeValueByteBufferSet final : public AttributeValueValue { + public: + explicit AttributeValueByteBufferSet(const Aws::Vector& value) : m_bS(value) {} + explicit AttributeValueByteBufferSet(Aws::Utils::Json::JsonView jsonValue); + const Aws::Vector GetBS() const override { return m_bS; } + void AddBItem(const Aws::Utils::ByteBuffer& bItem) override { m_bS.push_back(bItem); } + bool IsDefault() const override { return m_bS.empty(); } + bool operator==(const AttributeValueValue& other) const override; + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::BYTEBUFFER_SET; } + + private: + Aws::Vector m_bS; +}; + +/// Map Attribute Type +class AttributeValueMap final : public AttributeValueValue { + public: + explicit AttributeValueMap(const Aws::Map>& value) : m_m(value) {} + explicit AttributeValueMap(Aws::Utils::Json::JsonView jsonValue); + const Aws::Map> GetM() const override { return m_m; } + void AddMEntry(const Aws::String& key, const std::shared_ptr& value) override; + bool IsDefault() const override { return m_m.empty(); } + bool operator==(const AttributeValueValue& other) const override; + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::ATTRIBUTE_MAP; } + + private: + Aws::Map> m_m; +}; + +/// List Attribute Type +class AttributeValueList final : public AttributeValueValue { + public: + explicit AttributeValueList(const Aws::Vector>& value) : m_l(value) {} + explicit AttributeValueList(Aws::Utils::Json::JsonView jsonValue); + const Aws::Vector> GetL() const override { return m_l; } + void AddLItem(const std::shared_ptr& listItem) override { m_l.push_back(listItem); } + bool IsDefault() const override { return m_l.empty(); } + bool operator==(const AttributeValueValue& other) const override; + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::ATTRIBUTE_LIST; } + + private: + Aws::Vector> m_l; +}; + +/// Bool type +class AttributeValueBool final : public AttributeValueValue { + public: + explicit AttributeValueBool(bool value) : m_bool(value) {} + explicit AttributeValueBool(Aws::Utils::Json::JsonView jsonValue) : m_bool(jsonValue.GetBool("BOOL")) {} + bool GetBool() const override { return m_bool; } + bool IsDefault() const override { return m_bool == false; } + bool operator==(const AttributeValueValue& other) const override { return GetType() == other.GetType() && m_bool == other.GetBool(); } + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::BOOL; } + + private: + bool m_bool; +}; + +/// NULL type +class AttributeValueNull final : public AttributeValueValue { + public: + explicit AttributeValueNull(bool value) : m_null(value) {} + explicit AttributeValueNull(Aws::Utils::Json::JsonView jsonValue) : m_null(jsonValue.GetBool("NULL")) {} + bool GetNull() const override { return m_null; } + bool IsDefault() const override { return m_null == false; } + bool operator==(const AttributeValueValue& other) const override { return GetType() == other.GetType() && m_null == other.GetNull(); } + Aws::Utils::Json::JsonValue Jsonize() const override; + ValueType GetType() const override { return ValueType::NULLVALUE; } + + private: + bool m_null; +}; + +} // namespace Model +} // namespace DynamoDB +} // namespace Aws From 6222513ce2ab4076ffd93ba1441280d4e28258ac Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 27 Aug 2026 11:09:16 -0400 Subject: [PATCH 11/44] Smithy: DynamoDbRenderer emits bespoke AttributeValue; suppress default union render --- .../generators/model/ModelGenerator.java | 17 ++++- .../model/renderers/DynamoDbRenderer.java | 59 ++++++++++++++++++ .../model/renderers/DynamoDbRendererTest.java | 62 +++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRendererTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java index 1b07839704d..d9c23480747 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java @@ -7,6 +7,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.ClassifiedShapes; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.DynamoDbRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EnumShapeRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EventPayloadRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EventStreamRenderer; @@ -17,9 +18,11 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.protocol.ProtocolTraits; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; /** * Orchestrates C++ model code generation by dispatching classified shapes @@ -65,12 +68,24 @@ public void generateAll() { private List buildRenderers(ClassifiedShapes classified, RenderContext ctx) { List renderers = new ArrayList<>(); renderers.add(new EnumShapeRenderer(classified.enums(), ctx)); - renderers.add(new SubObjectRenderer(classified.subObjects(), classified.resultOutputIds(), ctx)); + + // DynamoDB's AttributeValue is a bespoke document type emitted by DynamoDbRenderer, not the + // generic union-struct SubObjectRenderer would produce. Drop it from the default sub-object + // set so it is not double-emitted. The shape stays in the model so member references resolve. + List subObjects = classified.subObjects(); + if ("dynamodb".equals(ctx.smithyServiceName())) { + subObjects = subObjects.stream() + .filter(s -> !"AttributeValue".equals(s.getId().getName())) + .collect(Collectors.toList()); + } + renderers.add(new SubObjectRenderer(subObjects, classified.resultOutputIds(), ctx)); + renderers.add(new RequestRenderer(classified.requests(), ctx)); renderers.add(new ResultRenderer(classified.results(), ctx)); renderers.add(new EventStreamRenderer(classified.eventStreamHandlers(), ctx)); renderers.add(new OutgoingEventStreamRenderer(classified.outgoingEventStreams(), ctx)); renderers.add(new EventPayloadRenderer(classified.blobPayloadEvents(), ctx)); + renderers.add(new DynamoDbRenderer(ctx)); return renderers; } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java new file mode 100644 index 00000000000..dd89c780818 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java @@ -0,0 +1,59 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Emits DynamoDB's bespoke document-type {@code AttributeValue} / {@code AttributeValueValue} + * classes, matching the legacy C2J {@code DynamoDBJsonCppClientGenerator}. The four files are + * static hand-written C++ (no model-driven content); their bodies live as classpath resources and + * are written verbatim. No-op for every non-DynamoDB service. The default union rendering of the + * {@code AttributeValue} shape is suppressed in {@code ModelGenerator}. + */ +public final class DynamoDbRenderer implements ShapeRenderer { + + private static final String RESOURCE_DIR = + "/com/amazonaws/util/awsclientsmithygenerator/generators/model/dynamodb/"; + + private final RenderContext ctx; + + public DynamoDbRenderer(RenderContext ctx) { + this.ctx = ctx; + } + + @Override + public void render(CppWriterDelegator writerDelegator) { + if (!"dynamodb".equals(ctx.smithyServiceName())) { + return; + } + emit(writerDelegator, "AttributeValue.h", "include/aws/dynamodb/model/AttributeValue.h"); + emit(writerDelegator, "AttributeValue.cpp", "source/model/AttributeValue.cpp"); + emit(writerDelegator, "AttributeValueValue.h", "include/aws/dynamodb/model/AttributeValueValue.h"); + emit(writerDelegator, "AttributeValueValue.cpp", "source/model/AttributeValueValue.cpp"); + } + + private void emit(CppWriterDelegator writerDelegator, String resourceName, String outputPath) { + String body = readResource(RESOURCE_DIR + resourceName); + writerDelegator.useFileWriter(outputPath, writer -> writer.writeWithNoFormatting(body)); + } + + private static String readResource(String path) { + try (InputStream in = DynamoDbRenderer.class.getResourceAsStream(path)) { + if (in == null) { + throw new IllegalStateException("Missing DynamoDB resource on classpath: " + path); + } + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Failed to read DynamoDB resource: " + path, e); + } + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRendererTest.java new file mode 100644 index 00000000000..f60dbf50a20 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRendererTest.java @@ -0,0 +1,62 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.protocol.ProtocolTraits; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that {@link DynamoDbRenderer} emits DynamoDB's bespoke {@code AttributeValue} / + * {@code AttributeValueValue} classes verbatim from classpath resources for the {@code dynamodb} + * service only, and is a no-op for every other service. + */ +class DynamoDbRendererTest { + + /** Builds a minimal {@link RenderContext} whose {@code smithyServiceName} is the given name. */ + private static RenderContext ctx(String smithyServiceName) { + ServiceShape service = ServiceShape.builder() + .id("com.example#Example") + .version("2024-01-01") + .build(); + Model model = Model.builder().addShape(service).build(); + ProtocolTraits traits = ProtocolResolver.traitsFor(Protocol.JSON); + return new RenderContext(model, service, traits, + "DynamoDB", "AWS_DYNAMODB_API", smithyServiceName); + } + + @Test + void emitsFourAttributeValueFilesForDynamoDb() { + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + new DynamoDbRenderer(ctx("dynamodb")).render(delegator); + delegator.flushWriters(); + assertTrue(manifest.hasFile("include/aws/dynamodb/model/AttributeValue.h")); + assertTrue(manifest.hasFile("source/model/AttributeValue.cpp")); + assertTrue(manifest.hasFile("include/aws/dynamodb/model/AttributeValueValue.h")); + assertTrue(manifest.hasFile("source/model/AttributeValueValue.cpp")); + // Body was emitted verbatim: the class + a distinctive static-content marker are present. + String header = manifest.getFileString("include/aws/dynamodb/model/AttributeValue.h").orElseThrow(); + assertTrue(header.contains("class AWS_DYNAMODB_API AttributeValue")); + assertTrue(header.contains("std::shared_ptr m_value;")); + } + + @Test + void noOpForOtherService() { + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + new DynamoDbRenderer(ctx("kinesis")).render(delegator); + delegator.flushWriters(); + assertTrue(manifest.getFiles().isEmpty(), "non-dynamodb service must emit nothing"); + } +} From 52d3bff70f14683cf9d8ea4ac4a07e310b7579bc Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 27 Aug 2026 11:24:19 -0400 Subject: [PATCH 12/44] Smithy: test ModelGenerator suppresses default AttributeValue render for DynamoDB --- .../generators/model/ModelGeneratorTest.java | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java new file mode 100644 index 00000000000..e3ed2ec2fb9 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java @@ -0,0 +1,147 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * End-to-end guard for {@link ModelGenerator#buildRenderers} dropping DynamoDB's {@code + * AttributeValue} union from the default sub-object set. + * + *

The suppression is load-bearing, not cosmetic: {@code CppWriterDelegator.useFileWriter} + * keys writers by filename via {@code computeIfAbsent}, so if {@code AttributeValue} were left in + * {@code subObjects}, {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.SubObjectRenderer} + * and {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.DynamoDbRenderer} + * would both resolve the same {@code include/aws/dynamodb/model/AttributeValue.h} key, share one + * writer, and APPEND — silently concatenating the generic tagged-union struct onto the bespoke + * document type with no error. This test runs {@code ModelGenerator} end-to-end and asserts the + * emitted header is the bespoke class only, never the generic union. The per-renderer tests + * ({@code DynamoDbRendererTest}, {@code SubObjectRendererTest}) do not exercise the wiring in + * {@code buildRenderers} where the double-emit would occur. + */ +class ModelGeneratorTest { + + private static final String ATTRIBUTE_VALUE_HEADER = "include/aws/dynamodb/model/AttributeValue.h"; + // A synthetic union member whose generic per-member accessor (produced by SubObjectRenderer) + // does not exist anywhere in the bespoke AttributeValue resource, so its presence/absence + // cleanly distinguishes generic-union output from the hand-written document type. + private static final String GENERIC_UNION_MARKER = "WithSyntheticProbe"; + // A member unique to the bespoke hand-written AttributeValue (holds the AttributeValueValue). + private static final String BESPOKE_MARKER = "std::shared_ptr m_value;"; + + /** + * A minimal model with a service, one operation, and an {@code AttributeValue} union + * referenced by the operation input. The union carries a synthetic member so that, were it + * rendered generically, {@link #GENERIC_UNION_MARKER} would appear in the output. + */ + private static Model model() { + StringShape str = StringShape.builder().id("com.amazonaws.dynamodb#Str").build(); + UnionShape attributeValue = UnionShape.builder() + .id("com.amazonaws.dynamodb#AttributeValue") + .addMember("syntheticProbe", str.getId()) + .addMember("otherProbe", str.getId()) + .build(); + // Input carries an AttributeValue member, so the union is reachable and classified as a + // sub-object (which buildRenderers must then drop for dynamodb). + StructureShape input = StructureShape.builder() + .id("com.amazonaws.dynamodb#DoThingInput") + .addMember("item", attributeValue.getId()) + .build(); + StructureShape output = StructureShape.builder() + .id("com.amazonaws.dynamodb#DoThingOutput") + .addMember("result", str.getId()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.amazonaws.dynamodb#DoThing") + .input(input.getId()) + .output(output.getId()) + .build(); + software.amazon.smithy.model.shapes.ServiceShape service = + software.amazon.smithy.model.shapes.ServiceShape.builder() + .id("com.amazonaws.dynamodb#DynamoDB_20120810") + .version("2012-08-10") + .addOperation(op.getId()) + .build(); + return Model.builder().addShapes(str, attributeValue, input, output, op, service).build(); + } + + private static MockManifest generate(String smithyServiceName, String namespace, String exportMacro) { + Model model = model(); + software.amazon.smithy.model.shapes.ServiceShape service = model.expectShape( + ShapeId.from("com.amazonaws.dynamodb#DynamoDB_20120810"), + software.amazon.smithy.model.shapes.ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + new ModelGenerator(model, service, delegator, smithyServiceName, exportMacro, namespace) + .generateAll(); + delegator.flushWriters(); + return manifest; + } + + private static long countFilesNamed(MockManifest manifest, String fileName) { + return manifest.getFiles().stream() + .filter(p -> fileName.equals(p.getFileName().toString())) + .count(); + } + + @Test + void dynamoDb_emitsBespokeAttributeValueOnly_notGenericUnion() { + MockManifest manifest = generate("dynamodb", "DynamoDB", "AWS_DYNAMODB_API"); + + // Exactly one AttributeValue.h is emitted (no second writer, no append/corruption). + assertEquals(1, countFilesNamed(manifest, "AttributeValue.h"), + "exactly one AttributeValue.h must be emitted: " + manifest.getFiles()); + assertTrue(manifest.hasFile(ATTRIBUTE_VALUE_HEADER), + "bespoke header must be at the dynamodb model path: " + manifest.getFiles()); + + String header = manifest.getFileString(ATTRIBUTE_VALUE_HEADER).orElseThrow(); + // It is the bespoke document type ... + assertTrue(header.contains(BESPOKE_MARKER), + "AttributeValue.h must be the bespoke document type: " + header); + // ... and NOT the generic tagged-union SubObjectRenderer would produce for the union + // members. Its presence would mean the generic body was (also) written to this file. + assertFalse(header.contains(GENERIC_UNION_MARKER), + "AttributeValue.h must not contain generic-union accessors (double-emit/corruption): " + + header); + + // The bespoke companion type is emitted too. + assertTrue(manifest.hasFile("include/aws/dynamodb/model/AttributeValueValue.h"), + "AttributeValueValue.h must be emitted: " + manifest.getFiles()); + + // The member carrier's request header still references AttributeValue (the shape stays in + // the model so member references resolve). + String request = manifest.getFileString("include/aws/dynamodb/model/DoThingRequest.h") + .orElseThrow(); + assertTrue(request.contains("AttributeValue"), + "request header must still reference AttributeValue: " + request); + } + + @Test + void otherService_rendersAttributeValueUnionGenerically() { + // Control: the suppression is dynamodb-specific. For any other service, DynamoDbRenderer is + // a no-op and the AttributeValue union flows through SubObjectRenderer as a generic union, + // proving the buildRenderers filter is what removes it for dynamodb. + MockManifest manifest = generate("kinesis", "Kinesis", "AWS_KINESIS_API"); + + String header = manifest.getFileString("include/aws/kinesis/model/AttributeValue.h") + .orElseThrow(); + assertTrue(header.contains(GENERIC_UNION_MARKER), + "non-dynamodb service must render AttributeValue as a generic union: " + header); + assertFalse(header.contains(BESPOKE_MARKER), + "non-dynamodb service must not emit the bespoke document type: " + header); + } +} From 2c2660d28e3e53a2f1cf37fa5bd1894c7f03bcd7 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 27 Aug 2026 11:47:10 -0400 Subject: [PATCH 13/44] Smithy: move DynamoDB AttributeValue suppression into a service-level transform --- .../generators/model/ModelCodegenPlugin.java | 4 +- .../generators/model/ModelGenerator.java | 15 +--- .../generators/model/ShapeClassifier.java | 8 +- .../model/transforms/CustomRenderedTrait.java | 34 +++++++ .../model/transforms/DynamoDbTransforms.java | 54 +++++++++++ .../generators/model/ModelGeneratorTest.java | 49 ++++++---- .../generators/model/ShapeClassifierTest.java | 47 ++++++++++ .../transforms/DynamoDbTransformsTest.java | 89 +++++++++++++++++++ 8 files changed, 267 insertions(+), 33 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index 99024a729a3..f5e2180392f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -9,6 +9,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.AccessAnalyzerTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayV2Transforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.DynamoDbTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.Ec2Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LambdaTransforms; @@ -56,7 +57,8 @@ public void execute(PluginContext context) { ApiGatewayTransforms.asTransform(), ApiGatewayV2Transforms.asTransform(), Ec2Transforms.asTransform(), - AccessAnalyzerTransforms.asTransform() + AccessAnalyzerTransforms.asTransform(), + DynamoDbTransforms.asTransform() // Future: S3Transforms.asTransform(), etc. )); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java index d9c23480747..52eba91a696 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java @@ -18,11 +18,9 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.protocol.ProtocolTraits; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.Shape; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; /** * Orchestrates C++ model code generation by dispatching classified shapes @@ -68,18 +66,7 @@ public void generateAll() { private List buildRenderers(ClassifiedShapes classified, RenderContext ctx) { List renderers = new ArrayList<>(); renderers.add(new EnumShapeRenderer(classified.enums(), ctx)); - - // DynamoDB's AttributeValue is a bespoke document type emitted by DynamoDbRenderer, not the - // generic union-struct SubObjectRenderer would produce. Drop it from the default sub-object - // set so it is not double-emitted. The shape stays in the model so member references resolve. - List subObjects = classified.subObjects(); - if ("dynamodb".equals(ctx.smithyServiceName())) { - subObjects = subObjects.stream() - .filter(s -> !"AttributeValue".equals(s.getId().getName())) - .collect(Collectors.toList()); - } - renderers.add(new SubObjectRenderer(subObjects, classified.resultOutputIds(), ctx)); - + renderers.add(new SubObjectRenderer(classified.subObjects(), classified.resultOutputIds(), ctx)); renderers.add(new RequestRenderer(classified.requests(), ctx)); renderers.add(new ResultRenderer(classified.results(), ctx)); renderers.add(new EventStreamRenderer(classified.eventStreamHandlers(), ctx)); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java index 0dc0eaf46ad..3f7c035a93b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java @@ -5,6 +5,7 @@ package com.amazonaws.util.awsclientsmithygenerator.generators.model; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.CustomRenderedTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; @@ -199,7 +200,12 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto subObjects.add(shape); } } else if (shape.isStructureShape() || shape.isUnionShape()) { - subObjects.add(shape); + // A shape marked @customRendered is emitted by a dedicated renderer (e.g. + // DynamoDbRenderer for AttributeValue); skip the default sub-object emission so the + // two do not both write — and append into — the same model file. + if (!shape.hasTrait(CustomRenderedTrait.class)) { + subObjects.add(shape); + } } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java new file mode 100644 index 00000000000..34e6187d997 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java @@ -0,0 +1,34 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal, synthetic marker trait stamped onto a shape by a service-level transform to signal that + * the shape is emitted by a dedicated {@code ShapeRenderer} (e.g. {@code DynamoDbRenderer}) rather + * than by the generic {@code SubObjectRenderer}. {@code ShapeClassifier} skips any structure/union + * bearing this trait, so the default sub-object body is never emitted for it — preventing the + * double-emit that {@code CppWriterDelegator}'s append-on-existing-writer behaviour would otherwise + * silently produce. + * + *

This trait is never declared in a Smithy model file; it exists only as an in-memory trait + * instance added inside a {@code ModelTransformer}. Smithy does not require a model-level trait + * definition for an in-memory instance because trait-definition validation runs only through the + * {@code ModelAssembler}, not through {@code Model.toBuilder().build()} / {@code ModelTransformer}. + * The synthetic {@code aws.cpp.internal} namespace keeps its id from ever colliding with a real + * modeled trait. + */ +public final class CustomRenderedTrait extends AnnotationTrait { + + /** The synthetic, internal-only id for this marker trait. */ + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#customRendered"); + + public CustomRenderedTrait() { + super(ID, ObjectNode.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java new file mode 100644 index 00000000000..e4be2f81216 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java @@ -0,0 +1,54 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; + +import java.util.Optional; + +/** + * DynamoDB model transform: marks the {@code AttributeValue} shape with {@link CustomRenderedTrait} + * so {@code ShapeClassifier} drops it from the default sub-object set. DynamoDB's {@code + * AttributeValue} is a bespoke document type emitted verbatim by {@code DynamoDbRenderer} (matching + * the legacy C2J {@code DynamoDBJsonCppClientGenerator}); if the generic {@code SubObjectRenderer} + * also emitted it, both writers would resolve the same {@code AttributeValue.h} path and append, + * silently concatenating the generic tagged-union struct onto the hand-written document type. + * + *

Keeping the suppression here — rather than as a service-name {@code if} in the generic + * {@code ModelGenerator} — keeps the orchestrator service-agnostic: the marker drives a generic + * classifier rule that applies to any shape a dedicated renderer owns. The shape itself stays in + * the model so member references (e.g. {@code PutItemInput.Item}) still resolve. + * + *

Self-guards on the raw smithy service name {@code dynamodb} (no-op for every other service). + * No-op when the {@code AttributeValue} shape is absent (upstream model changed). + */ +public final class DynamoDbTransforms { + + private DynamoDbTransforms() {} + + public static ModelTransform asTransform() { + return DynamoDbTransforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"dynamodb".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + ShapeId attributeValueId = ShapeId.fromParts(service.getId().getNamespace(), "AttributeValue"); + Optional attributeValue = model.getShape(attributeValueId); + if (attributeValue.isEmpty()) { + return model; // upstream model no longer defines AttributeValue: nothing to mark (no-op). + } + Shape marked = Shape.shapeToBuilder(attributeValue.get()) + .addTrait(new CustomRenderedTrait()) + .build(); + return model.toBuilder().addShape(marked).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java index e3ed2ec2fb9..0ff4553eb25 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java @@ -5,10 +5,13 @@ package com.amazonaws.util.awsclientsmithygenerator.generators.model; import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.DynamoDbTransforms; import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; @@ -19,8 +22,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * End-to-end guard for {@link ModelGenerator#buildRenderers} dropping DynamoDB's {@code - * AttributeValue} union from the default sub-object set. + * End-to-end guard for suppressing DynamoDB's {@code AttributeValue} union from the default + * sub-object set. Suppression is now driven by {@link DynamoDbTransforms} marking the shape + * {@code @customRendered} and {@link ShapeClassifier} skipping marked shapes — the generic + * {@code ModelGenerator} no longer knows about dynamodb. This test therefore applies the DynamoDB + * transform to its model before running {@code ModelGenerator}, exercising the whole chain: + * transform-marks -> classifier-skips -> single bespoke file. * *

The suppression is load-bearing, not cosmetic: {@code CppWriterDelegator.useFileWriter} * keys writers by filename via {@code computeIfAbsent}, so if {@code AttributeValue} were left in @@ -30,8 +37,8 @@ * writer, and APPEND — silently concatenating the generic tagged-union struct onto the bespoke * document type with no error. This test runs {@code ModelGenerator} end-to-end and asserts the * emitted header is the bespoke class only, never the generic union. The per-renderer tests - * ({@code DynamoDbRendererTest}, {@code SubObjectRendererTest}) do not exercise the wiring in - * {@code buildRenderers} where the double-emit would occur. + * ({@code DynamoDbRendererTest}, {@code SubObjectRendererTest}) do not exercise this wiring where + * the double-emit would occur. */ class ModelGeneratorTest { @@ -47,8 +54,12 @@ class ModelGeneratorTest { * A minimal model with a service, one operation, and an {@code AttributeValue} union * referenced by the operation input. The union carries a synthetic member so that, were it * rendered generically, {@link #GENERIC_UNION_MARKER} would appear in the output. + * + *

The service carries a {@code ServiceTrait} whose {@code sdkId} is {@code smithyServiceName} + * so {@link DynamoDbTransforms}' own self-guard (which reads the service's sdk id) fires + * consistently with the {@code smithyServiceName} passed to {@code ModelGenerator}. */ - private static Model model() { + private static Model model(String smithyServiceName) { StringShape str = StringShape.builder().id("com.amazonaws.dynamodb#Str").build(); UnionShape attributeValue = UnionShape.builder() .id("com.amazonaws.dynamodb#AttributeValue") @@ -56,7 +67,7 @@ private static Model model() { .addMember("otherProbe", str.getId()) .build(); // Input carries an AttributeValue member, so the union is reachable and classified as a - // sub-object (which buildRenderers must then drop for dynamodb). + // sub-object (which the transform+classifier must then drop for dynamodb). StructureShape input = StructureShape.builder() .id("com.amazonaws.dynamodb#DoThingInput") .addMember("item", attributeValue.getId()) @@ -70,23 +81,27 @@ private static Model model() { .input(input.getId()) .output(output.getId()) .build(); - software.amazon.smithy.model.shapes.ServiceShape service = - software.amazon.smithy.model.shapes.ServiceShape.builder() - .id("com.amazonaws.dynamodb#DynamoDB_20120810") - .version("2012-08-10") - .addOperation(op.getId()) - .build(); + ServiceShape service = ServiceShape.builder() + .id("com.amazonaws.dynamodb#DynamoDB_20120810") + .version("2012-08-10") + .addTrait(ServiceTrait.builder().sdkId(smithyServiceName).arnNamespace("dynamodb") + .cloudFormationName("DynamoDB").cloudTrailEventSource("dynamodb").build()) + .addOperation(op.getId()) + .build(); return Model.builder().addShapes(str, attributeValue, input, output, op, service).build(); } private static MockManifest generate(String smithyServiceName, String namespace, String exportMacro) { - Model model = model(); - software.amazon.smithy.model.shapes.ServiceShape service = model.expectShape( - ShapeId.from("com.amazonaws.dynamodb#DynamoDB_20120810"), - software.amazon.smithy.model.shapes.ServiceShape.class); + Model model = model(smithyServiceName); + ServiceShape service = model.expectShape( + ShapeId.from("com.amazonaws.dynamodb#DynamoDB_20120810"), ServiceShape.class); + // Apply the DynamoDB service-level transform first, mirroring the real ModelCodegenPlugin + // pipeline: for dynamodb it marks AttributeValue @customRendered; for any other service it + // is a no-op. Suppression then flows through ShapeClassifier, not ModelGenerator. + Model transformed = DynamoDbTransforms.asTransform().apply(model, service); MockManifest manifest = new MockManifest(); CppWriterDelegator delegator = new CppWriterDelegator(manifest); - new ModelGenerator(model, service, delegator, smithyServiceName, exportMacro, namespace) + new ModelGenerator(transformed, service, delegator, smithyServiceName, exportMacro, namespace) .generateAll(); delegator.flushWriters(); return manifest; diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java index d00b8c63544..946557260de 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java @@ -4,6 +4,7 @@ */ package com.amazonaws.util.awsclientsmithygenerator.generators.model; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.CustomRenderedTrait; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.*; @@ -570,6 +571,52 @@ void nonBlobEvent_staysSubObject() { "Non-blob event struct must NOT be a blob-payload event: " + classified.blobPayloadEvents()); } + /** + * A service whose operation input references two structs: one marked {@link CustomRenderedTrait} + * (owned by a dedicated renderer) and one plain. The classifier's generic marker rule must drop + * the marked one from subObjects while keeping the plain one — for any service, not just + * dynamodb. + */ + private Model modelWithCustomRenderedShape() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape marked = StructureShape.builder() + .id("com.example#MarkedThing") + .addMember("name", str.getId()) + .addTrait(new CustomRenderedTrait()) + .build(); + StructureShape plain = StructureShape.builder() + .id("com.example#PlainThing") + .addMember("name", str.getId()) + .build(); + StructureShape request = StructureShape.builder() + .id("com.example#DoRequest") + .addMember("marked", marked.getId()) + .addMember("plain", plain.getId()) + .build(); + StructureShape response = StructureShape.builder().id("com.example#DoResponse").build(); + OperationShape op = OperationShape.builder() + .id("com.example#Do").input(request.getId()).output(response.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2023-01-01").addOperation(op.getId()) + .addTrait(ServiceTrait.builder().sdkId("test").arnNamespace("test") + .cloudFormationName("Test").cloudTrailEventSource("test").build()) + .build(); + return Model.builder().addShapes(str, marked, plain, request, response, op, service).build(); + } + + @Test + void customRenderedShape_isExcludedFromSubObjects() { + Model model = modelWithCustomRenderedShape(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + assertTrue(classified.subObjects().stream() + .noneMatch(s -> s.getId().getName().equals("MarkedThing")), + "@customRendered shape must be dropped from subObjects: " + classified.subObjects()); + assertTrue(classified.subObjects().stream() + .anyMatch(s -> s.getId().getName().equals("PlainThing")), + "unmarked shape must remain a sub-object: " + classified.subObjects()); + } + @Test void classifiesEnumShape() { // StringShape with @enum trait -> classified as enum diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java new file mode 100644 index 00000000000..899681f7f22 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java @@ -0,0 +1,89 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies {@link DynamoDbTransforms} stamps {@link CustomRenderedTrait} onto DynamoDB's + * {@code AttributeValue} shape and is a no-op for other services / absent shapes. + * + *

Also serves as the empirical proof that Smithy accepts an in-memory trait instance + * with a synthetic, undefined id ({@code aws.cpp.internal#customRendered}) added inside a transform + * via {@code shapeToBuilder().addTrait(...)} + {@code model.toBuilder().build()} — no model-level + * trait definition required, because that build path does not run trait-definition validation. + */ +class DynamoDbTransformsTest { + + private static final String NS = "com.amazonaws.dynamodb"; + + private static Model model(String sdkId, boolean withAttributeValue) { + StringShape str = StringShape.builder().id(NS + "#Str").build(); + StructureShape.Builder inputB = StructureShape.builder().id(NS + "#PutItemInput"); + Model.Builder builder = Model.builder().addShape(str); + if (withAttributeValue) { + UnionShape attributeValue = UnionShape.builder() + .id(NS + "#AttributeValue") + .addMember("s", str.getId()) + .build(); + inputB.addMember("item", attributeValue.getId()); + builder.addShape(attributeValue); + } + StructureShape input = inputB.build(); + StructureShape output = StructureShape.builder().id(NS + "#PutItemOutput").build(); + OperationShape op = OperationShape.builder() + .id(NS + "#PutItem").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id(NS + "#DynamoDB_20120810").version("2012-08-10") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("dynamodb") + .cloudFormationName("DynamoDB").cloudTrailEventSource("dynamodb").build()) + .addOperation(op.getId()).build(); + return builder.addShapes(input, output, op, service).build(); + } + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from(NS + "#DynamoDB_20120810"), ServiceShape.class); + } + + @Test + void marksAttributeValueForDynamoDb() { + Model m = model("DynamoDB", true); + Model out = DynamoDbTransforms.asTransform().apply(m, service(m)); + + assertTrue(out.expectShape(ShapeId.from(NS + "#AttributeValue")) + .hasTrait(CustomRenderedTrait.class), + "AttributeValue must be marked @customRendered for dynamodb"); + } + + @Test + void noOpForOtherService() { + // A non-dynamodb service (sdkId resolves via getSmithyServiceName) is untouched. + Model m = model("Kinesis", true); + Model out = DynamoDbTransforms.asTransform().apply(m, service(m)); + assertSame(m, out, "transform must be a no-op for non-dynamodb services"); + assertFalse(out.expectShape(ShapeId.from(NS + "#AttributeValue")) + .hasTrait(CustomRenderedTrait.class), + "non-dynamodb AttributeValue must not be marked"); + } + + @Test + void noOpWhenAttributeValueAbsent() { + Model m = model("DynamoDB", false); + Model out = DynamoDbTransforms.asTransform().apply(m, service(m)); + assertSame(m, out, "transform must be a no-op when AttributeValue is absent"); + } +} From 911da25ea7588ec1819b7b2ce23016096bbc5c4e Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 27 Aug 2026 14:30:48 -0400 Subject: [PATCH 14/44] OperationContextParamsTrait handling and JmesPath parsing Smithy: failing test for OperationContextParams header declarations Smithy: port OperationContextCppCodeGenerator string-building helper Smithy: port CppEndpointsJmesPathVisitor onto Smithy shape types Smithy: unit tests for OperationContextParams JMESPath visitor Smithy: emit GetOperationContextParams header decl for OperationContextParamsTrait Smithy: emit GetEndpointContextParams body and GetOperationContextParams accessor Smithy: emit Accessor comment before GetOperationContextParams for C2J parity Smithy: end-to-end tests for OperationContextParams JMESPath patterns Smithy: reduce OperationContextParams visitor boilerplate via UnsupportedExpressionVisitor base Smithy: whitespace-tolerant assertions for OperationContextParams visitor tests Smithy: immutable Emit-based OperationContextParams visitor; drop mutable code generator Smithy: correct stale accessor-emission comment after immutable redesign --- .../model/renderers/RequestRenderer.java | 57 ++++- .../model/renderers/endpointcontext/Emit.java | 20 ++ .../SmithyEndpointsJmesPathVisitor.java | 145 +++++++++++++ .../generators/model/RequestRendererTest.java | 199 ++++++++++++++++++ .../SmithyEndpointsJmesPathVisitorTest.java | 158 ++++++++++++++ 5 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/Emit.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index 3751644a118..8622fafc506 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -14,6 +14,9 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.RequestInfo; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.Emit; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.SmithyEndpointsJmesPathVisitor; +import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.model.node.BooleanNode; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.NodeVisitor; @@ -26,6 +29,8 @@ import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait; import software.amazon.smithy.model.traits.RequestCompressionTrait; import software.amazon.smithy.rulesengine.traits.ContextParamTrait; +import software.amazon.smithy.rulesengine.traits.OperationContextParamDefinition; +import software.amazon.smithy.rulesengine.traits.OperationContextParamsTrait; import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait; import java.util.ArrayList; @@ -187,6 +192,9 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.write(" * Helper function to collect parameters (configurable and static hardcoded) required for endpoint computation."); writer.write(" */"); writer.write("$L EndpointParameters GetEndpointContextParams() const override;", ctx.exportMacro()); + if (operation.hasTrait(OperationContextParamsTrait.class)) { + writer.write("$L Aws::Vector GetOperationContextParams() const;", ctx.exportMacro()); + } } writer.write(""); @@ -272,6 +280,9 @@ private void renderSource(CppWriterDelegator writerDelegator, if (hasEndpointContextParams(operation, shape)) { writer.write(""); renderEndpointContextParams(writer, className, operation, shape); + if (operation.hasTrait(OperationContextParamsTrait.class)) { + renderOperationContextParamsAccessor(writer, className, operation, shape); + } } writer.write(""); return; @@ -293,6 +304,9 @@ private void renderSource(CppWriterDelegator writerDelegator, if (hasEndpointContextParams(operation, shape)) { writer.write(""); renderEndpointContextParams(writer, className, operation, shape); + if (operation.hasTrait(OperationContextParamsTrait.class)) { + renderOperationContextParamsAccessor(writer, className, operation, shape); + } } writer.write(""); }); @@ -546,7 +560,8 @@ private static void validateGzipEncoding(OperationShape operation) { } private boolean hasEndpointContextParams(OperationShape operation, StructureShape shape) { - if (operation.hasTrait(StaticContextParamsTrait.class)) { + if (operation.hasTrait(StaticContextParamsTrait.class) + || operation.hasTrait(OperationContextParamsTrait.class)) { return true; } for (MemberShape member : shape.getAllMembers().values()) { @@ -589,10 +604,50 @@ private void renderEndpointContextParams(CppWriter writer, String className, } } + if (operation.hasTrait(OperationContextParamsTrait.class)) { + OperationContextParamsTrait opCtx = operation.expectTrait(OperationContextParamsTrait.class); + Map.Entry firstEntry = + opCtx.getParameters().entrySet().iterator().next(); + writer.write("// operation context params go here"); + writer.write( + "parameters.emplace_back(Aws::String{\"$L\"}, this->GetOperationContextParams(), " + + "Aws::Endpoint::EndpointParameter::ParameterOrigin::OPERATION_CONTEXT);", + firstEntry.getKey()); + } + writer.write("return parameters;"); }); } + private void renderOperationContextParamsAccessor(CppWriter writer, String className, + OperationShape operation, StructureShape shape) { + OperationContextParamsTrait opCtx = operation.expectTrait(OperationContextParamsTrait.class); + Map.Entry firstEntry = + opCtx.getParameters().entrySet().iterator().next(); + String path = firstEntry.getValue().getPath(); + + Emit emit = JmespathExpression.parse(path).accept( + new SmithyEndpointsJmesPathVisitor(this.ctx.model(), shape, "(*this)")); + + writer.write("// Accessor for dynamic context endpoint params"); + writer.openBlock("Aws::Vector $L::GetOperationContextParams() const {", "}", + className, () -> { + writer.write("Aws::Vector result;"); + // Visitor output is a single newline-separated string of flat (un-indented) statements; + // CppWriter applies the block indentation and clang-format normalizes the rest. Each line + // is passed as a $L argument so any $L or {n} tokens in the output are not reinterpreted. + String raw = emit.statements(); + if (!raw.isEmpty()) { + // Trim the single trailing newline the visitor always emits so we don't double-blank. + String trimmed = raw.endsWith("\n") ? raw.substring(0, raw.length() - 1) : raw; + for (String line : trimmed.split("\n", -1)) { + writer.write("$L", line); + } + } + writer.write("return result;"); + }); + } + private void appendStaticContextParam(CppWriter writer, String name, Node value) { value.accept(new NodeVisitor.Default() { @Override diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/Emit.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/Emit.java new file mode 100644 index 00000000000..5a7689c5e99 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/Emit.java @@ -0,0 +1,20 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext; + +import software.amazon.smithy.model.shapes.Shape; + +/** + * Immutable result of visiting one JMESPath node while building the body of + * {@code GetOperationContextParams()}. + * + * @param statements fully-formed C++ statement lines emitted at/under this node (may be empty) + * @param valueExpr C++ expression for the value located by the path so far (parents extend it) + * @param shape the Smithy shape resolved at this node (may be null at a terminal) + * @param rootName leading identifier of the current accessor chain (first field's name); + * the alias is {@code rootName + "Elems"} and a projection loop var is + * {@code rootName + "Elem"} + */ +public record Emit(String statements, String valueExpr, Shape shape, String rootName) {} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java new file mode 100644 index 00000000000..bc141d2142b --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java @@ -0,0 +1,145 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext; + +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppNames; +import com.amazonaws.util.awsclientsmithygenerator.generators.waiters.jmespath.UnsupportedExpressionVisitor; +import software.amazon.smithy.build.SmithyBuildException; +import software.amazon.smithy.jmespath.ast.ExpressionTypeExpression; +import software.amazon.smithy.jmespath.ast.FieldExpression; +import software.amazon.smithy.jmespath.ast.FlattenExpression; +import software.amazon.smithy.jmespath.ast.FunctionExpression; +import software.amazon.smithy.jmespath.ast.MultiSelectListExpression; +import software.amazon.smithy.jmespath.ast.ProjectionExpression; +import software.amazon.smithy.jmespath.ast.Subexpression; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; + +/** + * Translates the JMESPath expression carried by {@code smithy.rules#operationContextParams} + * into C++ that walks the request struct and pushes leaf values into a {@code result} + * {@code Aws::Vector}. Immutable and value-returning: each visit returns an + * {@link Emit}; parents compose children's results. Produces the same C++ structure as the + * legacy C2J generator (identifiers and statements), modulo whitespace (normalized downstream + * by clang-format). + */ +public final class SmithyEndpointsJmesPathVisitor extends UnsupportedExpressionVisitor { + + private final Model model; + private final Shape input; + private final String baseExpr; + private final String chainRoot; // null => the next field starts a new chain and names the alias + + /** Entry point: {@code baseExpr} is typically {@code "(*this)"}; the chain is not yet started. */ + public SmithyEndpointsJmesPathVisitor(Model model, Shape input, String baseExpr) { + this(model, input, baseExpr, null); + } + + private SmithyEndpointsJmesPathVisitor(Model model, Shape input, String baseExpr, String chainRoot) { + this.model = model; + this.input = input; + this.baseExpr = baseExpr; + this.chainRoot = chainRoot; + } + + @Override + public Emit visitField(FieldExpression expression) { + if (!(input instanceof StructureShape)) { + throw new SmithyBuildException("Failed to get field from expression"); + } + MemberShape member = ((StructureShape) input).getMember(expression.getName()).orElse(null); + if (member == null) { + throw new SmithyBuildException("Failed to get field from expression"); + } + String root = (chainRoot != null) ? chainRoot : expression.getName(); + String expr = baseExpr + ".Get" + CppNames.capitalize(expression.getName()) + "()"; + Shape target = model.expectShape(member.getTarget()); + if (target instanceof StructureShape) { + // Non-leaf: continue the accessor chain, emit nothing yet. + return new Emit("", expr, target, root); + } + String alias = root + "Elems"; + if (target.isStringShape()) { + String stmt = "auto& " + alias + " = " + expr + ";\n" + + "result.emplace_back(" + alias + ");\n"; + return new Emit(stmt, expr, target, root); + } + // List/map terminal of a chain consumed by an enclosing projection/keys: bind the alias only. + return new Emit("auto& " + alias + " = " + expr + ";\n", expr, target, root); + } + + @Override + public Emit visitSubexpression(Subexpression expression) { + Emit left = expression.getLeft().accept(this); + Emit right = expression.getRight().accept( + new SmithyEndpointsJmesPathVisitor(model, left.shape(), left.valueExpr(), left.rootName())); + return new Emit(left.statements() + right.statements(), + right.valueExpr(), right.shape(), right.rootName()); + } + + @Override + public Emit visitProjection(ProjectionExpression expression) { + Emit left = expression.getLeft().accept(this); + if (!(left.shape() instanceof ListShape)) { + // No list to iterate at this node (e.g. the trailing flatten-projection of a + // multi-select pattern, whose left subtree already emitted every statement). + // Propagate the left subtree's statements rather than discarding them. + return left; + } + String alias = left.rootName() + "Elems"; + String loopVar = left.rootName() + "Elem"; + Shape listMember = model.expectShape(((ListShape) left.shape()).getMember().getTarget()); + Emit body = expression.getRight().accept( + new SmithyEndpointsJmesPathVisitor(model, listMember, loopVar, null)); + String loop = "for (auto& " + loopVar + " : " + alias + ")\n{\n" + body.statements() + "}\n"; + return new Emit(left.statements() + loop, loopVar, listMember, null); + } + + @Override + public Emit visitFlatten(FlattenExpression expression) { + if (expression.getExpression() instanceof ProjectionExpression) { + return visitProjection((ProjectionExpression) expression.getExpression()); + } + return expression.getExpression().accept(this); + } + + @Override + public Emit visitFunction(FunctionExpression expression) { + if (!expression.getName().equals("keys")) { + throw new SmithyBuildException("Unsupported JMESPath expression"); + } + Emit arg = expression.getArguments().get(0).accept(this); + if (!(arg.shape() instanceof MapShape)) { + throw new SmithyBuildException("keys function not associated with Map type"); + } + MemberShape keyMember = ((MapShape) arg.shape()).getKey(); + if (!model.expectShape(keyMember.getTarget()).isStringShape()) { + throw new SmithyBuildException("map key of type other than string is not supported"); + } + String alias = arg.rootName() + "Elems"; + String loopVar = expression.getName() + "Elem"; // "keysElem" + String loop = "for (auto& " + loopVar + " : " + alias + ")\n{\n" + + "result.emplace_back(" + loopVar + ".first);\n}\n"; + return new Emit(arg.statements() + loop, loopVar, null, null); + } + + @Override + public Emit visitMultiSelectList(MultiSelectListExpression expression) { + StringBuilder sb = new StringBuilder(); + for (var e : expression.getExpressions()) { + sb.append(e.accept(this).statements()); + } + return new Emit(sb.toString(), baseExpr, input, null); + } + + @Override + public Emit visitExpressionType(ExpressionTypeExpression expression) { + return expression.getExpression().accept(this); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index fa361415318..aa52eca9798 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -11,6 +11,8 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -19,6 +21,8 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.rulesengine.traits.OperationContextParamDefinition; +import software.amazon.smithy.rulesengine.traits.OperationContextParamsTrait; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -661,4 +665,199 @@ void rawStreamingPayloadRequestRestJson_headerAndSourceAgreeOnRequestSpecificHea assertFalse(c.contains("GetRequestSpecificHeaders"), "Source must not define GetRequestSpecificHeaders (contentType is stripped): " + c); } + + // --- smithy.rules#operationContextParams --- + + /** Operation carrying ONLY smithy.rules#operationContextParams (no static, no member-level). */ + private static Model operationContextParamsOnlyModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + // Map for keys(RequestItems) — the JMESPath the trait resolves. + MapShape requestItemsMap = MapShape.builder() + .id("com.example#RequestItemsMap") + .key(MemberShape.builder().id("com.example#RequestItemsMap$key").target(str.getId()).build()) + .value(MemberShape.builder().id("com.example#RequestItemsMap$value").target(str.getId()).build()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoBatchInput") + .addMember("RequestItems", requestItemsMap.getId()) + .build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoBatchOutput").addMember("r", str.getId()).build(); + OperationContextParamsTrait ctxTrait = OperationContextParamsTrait.builder() + .putParameter("ResourceArnList", + OperationContextParamDefinition.builder().path("keys(RequestItems)").build()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoBatch").input(input.getId()).output(output.getId()) + .addTrait(ctxTrait).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, requestItemsMap, input, output, op, service).build(); + } + + private static String renderOperationContextRequest(Model model, String fileSuffix) { + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + Protocol protocol = ProtocolResolver.resolve(service, model); + new RequestRenderer( + ShapeClassifier.classify(model, service, protocol).requests(), + new RenderContext(model, service, ProtocolResolver.traitsFor(protocol), + "Example", "AWS_EXAMPLE_API", "example")).render(delegator); + delegator.flushWriters(); + return manifest.getFileString(manifest.getFiles().stream() + .filter(p -> p.toString().endsWith(fileSuffix)).findFirst().orElseThrow()).orElseThrow(); + } + + @Test + void operationContextParams_headerDeclaresGetters() { + // An operation carrying only smithy.rules#operationContextParams must produce both the + // GetEndpointContextParams() virtual override and the GetOperationContextParams() accessor + // in its request header. Fails on main because RequestRenderer ignores the trait. + String h = renderOperationContextRequest(operationContextParamsOnlyModel(), "DoBatchRequest.h"); + assertTrue(h.contains("EndpointParameters GetEndpointContextParams() const override;"), + "Missing GetEndpointContextParams decl: " + h); + assertTrue(h.contains("Aws::Vector GetOperationContextParams() const;"), + "Missing GetOperationContextParams decl: " + h); + } + + /** Struct-dot-string: TableCreationParameters.TableName. */ + private static Model operationContextParams_dotAccessModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape tcp = StructureShape.builder() + .id("com.example#TableCreationParameters").addMember("TableName", str.getId()).build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoBatchInput") + .addMember("TableCreationParameters", tcp.getId()).build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoBatchOutput").addMember("r", str.getId()).build(); + OperationContextParamsTrait trait = OperationContextParamsTrait.builder() + .putParameter("ResourceArn", + OperationContextParamDefinition.builder().path("TableCreationParameters.TableName").build()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoBatch").input(input.getId()).output(output.getId()).addTrait(trait).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, tcp, input, output, op, service).build(); + } + + /** List-projection-dot-string: TransactItems[*].Get.TableName. */ + private static Model operationContextParams_projectionModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape get = StructureShape.builder() + .id("com.example#Get").addMember("TableName", str.getId()).build(); + StructureShape item = StructureShape.builder() + .id("com.example#Item").addMember("Get", get.getId()).build(); + ListShape list = ListShape.builder() + .id("com.example#Items") + .member(MemberShape.builder().id("com.example#Items$member").target(item.getId()).build()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoBatchInput").addMember("TransactItems", list.getId()).build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoBatchOutput").addMember("r", str.getId()).build(); + OperationContextParamsTrait trait = OperationContextParamsTrait.builder() + .putParameter("ResourceArnList", + OperationContextParamDefinition.builder().path("TransactItems[*].Get.TableName").build()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoBatch").input(input.getId()).output(output.getId()).addTrait(trait).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, get, item, list, input, output, op, service).build(); + } + + /** Multi-select-list flatten: TransactItems[*].[ConditionCheck.TableName, Put.TableName, Delete.TableName, Update.TableName][]. */ + private static Model operationContextParams_multiSelectFlattenModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape cc = StructureShape.builder() + .id("com.example#CC").addMember("TableName", str.getId()).build(); + StructureShape put = StructureShape.builder() + .id("com.example#Put").addMember("TableName", str.getId()).build(); + StructureShape del = StructureShape.builder() + .id("com.example#Delete").addMember("TableName", str.getId()).build(); + StructureShape upd = StructureShape.builder() + .id("com.example#Update").addMember("TableName", str.getId()).build(); + StructureShape item = StructureShape.builder() + .id("com.example#Item") + .addMember("ConditionCheck", cc.getId()) + .addMember("Put", put.getId()) + .addMember("Delete", del.getId()) + .addMember("Update", upd.getId()).build(); + ListShape list = ListShape.builder() + .id("com.example#Items") + .member(MemberShape.builder().id("com.example#Items$member").target(item.getId()).build()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoBatchInput").addMember("TransactItems", list.getId()).build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoBatchOutput").addMember("r", str.getId()).build(); + OperationContextParamsTrait trait = OperationContextParamsTrait.builder() + .putParameter("ResourceArnList", + OperationContextParamDefinition.builder() + .path("TransactItems[*].[ConditionCheck.TableName, Put.TableName, Delete.TableName, Update.TableName][]").build()) + .build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoBatch").input(input.getId()).output(output.getId()).addTrait(trait).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, cc, put, del, upd, item, list, input, output, op, service).build(); + } + + @Test + void operationContextParams_keysPattern_endToEnd() { + // Uses Task 1's operationContextParamsOnlyModel() (keys(RequestItems)). + String h = renderOperationContextRequest(operationContextParamsOnlyModel(), "DoBatchRequest.h"); + String c = renderOperationContextRequest(operationContextParamsOnlyModel(), "DoBatchRequest.cpp"); + assertTrue(h.contains("EndpointParameters GetEndpointContextParams() const override;"), h); + assertTrue(h.contains("Aws::Vector GetOperationContextParams() const;"), h); + assertTrue(c.contains( + "parameters.emplace_back(Aws::String{\"ResourceArnList\"}, this->GetOperationContextParams()"), c); + assertTrue(c.contains("Aws::Vector DoBatchRequest::GetOperationContextParams() const"), c); + assertTrue(c.contains("auto& RequestItemsElems = (*this).GetRequestItems();"), c); + assertTrue(c.contains("for (auto& keysElem : RequestItemsElems)"), c); + assertTrue(c.contains("result.emplace_back(keysElem.first);"), c); + } + + @Test + void operationContextParams_dotAccessPattern_endToEnd() { + Model model = operationContextParams_dotAccessModel(); + String h = renderOperationContextRequest(model, "DoBatchRequest.h"); + String c = renderOperationContextRequest(model, "DoBatchRequest.cpp"); + assertTrue(h.contains("Aws::Vector GetOperationContextParams() const;"), h); + assertTrue(c.contains( + "parameters.emplace_back(Aws::String{\"ResourceArn\"}, this->GetOperationContextParams()"), c); + assertTrue(c.contains( + "auto& TableCreationParametersElems = (*this).GetTableCreationParameters().GetTableName();"), c); + assertTrue(c.contains("result.emplace_back(TableCreationParametersElems);"), c); + assertFalse(c.contains("for (auto&"), + "Dot-access pattern must not emit a for-loop: " + c); + } + + @Test + void operationContextParams_projectionPattern_endToEnd() { + Model model = operationContextParams_projectionModel(); + String c = renderOperationContextRequest(model, "DoBatchRequest.cpp"); + assertTrue(c.contains("auto& TransactItemsElems = (*this).GetTransactItems();"), c); + assertTrue(c.contains("for (auto& TransactItemsElem : TransactItemsElems)"), c); + assertTrue(c.contains( + "auto& GetElems = TransactItemsElem.GetGet().GetTableName();"), c); + assertTrue(c.contains("result.emplace_back(GetElems);"), c); + } + + @Test + void operationContextParams_multiSelectFlattenPattern_endToEnd() { + Model model = operationContextParams_multiSelectFlattenModel(); + String c = renderOperationContextRequest(model, "DoBatchRequest.cpp"); + assertTrue(c.contains("for (auto& TransactItemsElem : TransactItemsElems)"), c); + // Each of the four field-access branches must appear inside the loop body. + for (String branch : java.util.List.of("ConditionCheck", "Put", "Delete", "Update")) { + assertTrue(c.contains( + "auto& " + branch + "Elems = TransactItemsElem.Get" + branch + "().GetTableName();"), + "Missing branch " + branch + ": " + c); + assertTrue(c.contains("result.emplace_back(" + branch + "Elems);"), + "Missing result push for " + branch + ": " + c); + } + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java new file mode 100644 index 00000000000..9de7100e3f7 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java @@ -0,0 +1,158 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SmithyEndpointsJmesPathVisitorTest { + + private static final String STR = "com.example#String"; + + private static StringShape str() { + return StringShape.builder().id(STR).build(); + } + + private static String render(Model model, StructureShape input, String jmesPath) { + return JmespathExpression.parse(jmesPath) + .accept(new SmithyEndpointsJmesPathVisitor(model, input, "(*this)")) + .statements(); + } + + /** Collapse all runs of whitespace to a single space and trim, so comparisons ignore + * indentation/newlines (clang-format normalizes generated whitespace downstream). */ + private static String normalizeWs(String s) { + return s.replaceAll("\\s+", " ").trim(); + } + + @Test + void keysPattern() { + // Map under a request struct member "RequestItems". + MapShape reqMap = MapShape.builder() + .id("com.example#RequestItemsMap") + .key(MemberShape.builder().id("com.example#RequestItemsMap$key").target(STR).build()) + .value(MemberShape.builder().id("com.example#RequestItemsMap$value").target(STR).build()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#Req") + .addMember("RequestItems", reqMap.getId()).build(); + Model model = Model.builder().addShapes(str(), reqMap, input).build(); + + String expected = + "auto& RequestItemsElems = (*this).GetRequestItems();\n" + + "for (auto& keysElem : RequestItemsElems)\n" + + "{\n" + + "\tresult.emplace_back(keysElem.first);\n" + + "}\n"; + assertEquals(normalizeWs(expected), normalizeWs(render(model, input, "keys(RequestItems)"))); + } + + @Test + void dotAccessPattern() { + // Struct with member TableCreationParameters (Struct with member TableName: String). + StructureShape tcp = StructureShape.builder() + .id("com.example#TableCreationParameters") + .addMember("TableName", ShapeId.from(STR)).build(); + StructureShape input = StructureShape.builder() + .id("com.example#Req") + .addMember("TableCreationParameters", tcp.getId()).build(); + Model model = Model.builder().addShapes(str(), tcp, input).build(); + + String expected = + "auto& TableCreationParametersElems = (*this).GetTableCreationParameters().GetTableName();\n" + + "result.emplace_back(TableCreationParametersElems);\n"; + assertEquals(normalizeWs(expected), normalizeWs(render(model, input, "TableCreationParameters.TableName"))); + } + + @Test + void projectionPattern() { + // List>> under member TransactItems. + StructureShape getStruct = StructureShape.builder() + .id("com.example#Get").addMember("TableName", ShapeId.from(STR)).build(); + StructureShape item = StructureShape.builder() + .id("com.example#Item").addMember("Get", getStruct.getId()).build(); + ListShape list = ListShape.builder() + .id("com.example#Items") + .member(MemberShape.builder().id("com.example#Items$member").target(item.getId()).build()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#Req").addMember("TransactItems", list.getId()).build(); + Model model = Model.builder().addShapes(str(), getStruct, item, list, input).build(); + + String expected = + "auto& TransactItemsElems = (*this).GetTransactItems();\n" + + "for (auto& TransactItemsElem : TransactItemsElems)\n" + + "{\n" + + "\tauto& GetElems = TransactItemsElem.GetGet().GetTableName();\n" + + "\tresult.emplace_back(GetElems);\n" + + "}\n"; + assertEquals(normalizeWs(expected), normalizeWs(render(model, input, "TransactItems[*].Get.TableName"))); + } + + @Test + void multiSelectListFlattenPattern() { + // Same list-of-Item as projectionPattern but Item has four sibling struct members. + StructureShape cc = StructureShape.builder().id("com.example#CC").addMember("TableName", ShapeId.from(STR)).build(); + StructureShape put = StructureShape.builder().id("com.example#Put").addMember("TableName", ShapeId.from(STR)).build(); + StructureShape del = StructureShape.builder().id("com.example#Delete").addMember("TableName", ShapeId.from(STR)).build(); + StructureShape upd = StructureShape.builder().id("com.example#Update").addMember("TableName", ShapeId.from(STR)).build(); + StructureShape item = StructureShape.builder() + .id("com.example#Item") + .addMember("ConditionCheck", cc.getId()) + .addMember("Put", put.getId()) + .addMember("Delete", del.getId()) + .addMember("Update", upd.getId()).build(); + ListShape list = ListShape.builder() + .id("com.example#Items") + .member(MemberShape.builder().id("com.example#Items$member").target(item.getId()).build()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#Req").addMember("TransactItems", list.getId()).build(); + Model model = Model.builder().addShapes(str(), cc, put, del, upd, item, list, input).build(); + + String expected = + "auto& TransactItemsElems = (*this).GetTransactItems();\n" + + "for (auto& TransactItemsElem : TransactItemsElems)\n" + + "{\n" + + "\tauto& ConditionCheckElems = TransactItemsElem.GetConditionCheck().GetTableName();\n" + + "\tresult.emplace_back(ConditionCheckElems);\n" + + "\tauto& PutElems = TransactItemsElem.GetPut().GetTableName();\n" + + "\tresult.emplace_back(PutElems);\n" + + "\tauto& DeleteElems = TransactItemsElem.GetDelete().GetTableName();\n" + + "\tresult.emplace_back(DeleteElems);\n" + + "\tauto& UpdateElems = TransactItemsElem.GetUpdate().GetTableName();\n" + + "\tresult.emplace_back(UpdateElems);\n" + + "}\n"; + String actual = render(model, input, + "TransactItems[*].[ConditionCheck.TableName, Put.TableName, Delete.TableName, Update.TableName][]"); + assertEquals(normalizeWs(expected), normalizeWs(actual)); + } + + @Test + void unsupportedNode_throws() { + // A JMESPath expression exercising an unsupported node (filter projection) must throw + // UnsupportedOperationException, inherited from UnsupportedExpressionVisitor: fail fast + // on unrecognized constructs. + StructureShape input = StructureShape.builder() + .id("com.example#Req").addMember("x", ShapeId.from(STR)).build(); + Model model = Model.builder().addShapes(str(), input).build(); + + UnsupportedOperationException ex = assertThrows(UnsupportedOperationException.class, + () -> render(model, input, "x[?y == 'z']")); + // Only assert the prefix — the node-type suffix is intentionally not pinned. + assertTrue(ex.getMessage().startsWith("Unsupported expression:")); + } +} From 665c5b676b67466f5af850e31dc73716cfd28d5a Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 10:52:27 -0400 Subject: [PATCH 15/44] Smithy: scaffold S3Transforms and register in ModelCodegenPlugin --- .../generators/model/ModelCodegenPlugin.java | 6 ++- .../model/transforms/S3Transforms.java | 37 +++++++++++++ .../model/transforms/S3TransformsTest.java | 52 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index f5e2180392f..b38e4425aa6 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -13,6 +13,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.Ec2Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LambdaTransforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.S3Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SourceRegionTransform; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SqsTransforms; import software.amazon.smithy.build.PluginContext; @@ -58,8 +59,9 @@ public void execute(PluginContext context) { ApiGatewayV2Transforms.asTransform(), Ec2Transforms.asTransform(), AccessAnalyzerTransforms.asTransform(), - DynamoDbTransforms.asTransform() - // Future: S3Transforms.asTransform(), etc. + DynamoDbTransforms.asTransform(), + S3Transforms.asTransform() + // Future: S3ControlTransforms.asTransform(), etc. )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java new file mode 100644 index 00000000000..2ef81b2ca3c --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -0,0 +1,37 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; + +/** + * S3 (and S3-CRT, which shares the S3 model) parity with the legacy C2J + * {@code S3RestXmlCppClientGenerator} for the {@code Model::} namespace. Composes the S3 model + * mutations that C2J applies in {@code generateSourceFiles}. Self-guards on the raw smithy service + * name; every sub-transform no-ops when its target shapes are absent and fast-fails on genuine + * collisions. Client/endpoint/ARN/S3Express/CRT customizations are out of scope (separate + * generators), as is serde-body emission (still stubbed plugin-wide). + */ +public final class S3Transforms { + + private S3Transforms() {} + + public static ModelTransform asTransform() { + return S3Transforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + String name = ServiceNameUtil.getSmithyServiceName(service, null); + if (!"s3".equals(name) && !"s3-crt".equals(name)) { + return model; + } + // Sub-transforms are chained here by later tasks, e.g.: + // return normalizeReplicationStatus(expandBucketLocationConstraint(... (model) ...)); + return model; + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java new file mode 100644 index 00000000000..c09404da4ef --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -0,0 +1,52 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; + +import static org.junit.jupiter.api.Assertions.*; + +class S3TransformsTest { + + static final String NS = "com.amazonaws.s3"; + + static ServiceShape s3Service(String sdkId) { + return ServiceShape.builder().id(NS + "#AmazonS3").version("2006-03-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("s3") + .cloudFormationName("S3").cloudTrailEventSource("s3.amazonaws.com").build()) + .build(); + } + + static Model modelWith(ServiceShape svc, software.amazon.smithy.model.shapes.Shape... shapes) { + Model.Builder b = Model.builder().addShape(svc); + for (software.amazon.smithy.model.shapes.Shape s : shapes) b.addShape(s); + return b.build(); + } + + @Test + void noOpForOtherService() { + ServiceShape svc = ServiceShape.builder().id("com.amazonaws.other#Other").version("1") + .addTrait(ServiceTrait.builder().sdkId("Other").arnNamespace("other") + .cloudFormationName("Other").cloudTrailEventSource("other").build()).build(); + Model m = Model.builder().addShape(svc).build(); + Model out = S3Transforms.asTransform().apply(m, svc); + assertSame(m, out, "non-s3 service must be untouched"); + } + + @Test + void noOpForS3WhenNothingToDo() { + ServiceShape svc = s3Service("S3"); + Model m = modelWith(svc); + // Scaffold has no sub-transforms yet: s3 model returns unchanged (structurally equal). + Model out = S3Transforms.asTransform().apply(m, svc); + assertNotNull(out); + assertTrue(out.getShape(ShapeId.from(NS + "#AmazonS3")).isPresent()); + } +} From 54352dc9afca62ec795ee35ea898602a85ef6d5c Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 10:58:27 -0400 Subject: [PATCH 16/44] Smithy: S3Transforms renames CopyObjectResult to CopyObjectResultDetails --- .../generators/ShapeUtil.java | 15 ------------ .../model/transforms/S3Transforms.java | 19 ++++++++++++++- .../model/transforms/S3TransformsTest.java | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java index c36f1c5d9d8..ccfc16f6b05 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java @@ -78,21 +78,6 @@ public class ShapeUtil { "s3", Map.of("CopyObjectResult", "CopyObjectResultDetails") ); - /** - * S3 shapes that exist in C2J but not in Smithy. - * These must be synthetically injected into the model before generation. - */ - public static final Map> C2J_ONLY_SHAPES = Map.of( - "s3", Set.of( - "CopyObjectResultDetails", "SelectObjectContentEventStreamUnmarshallerError", - "CloudFunctionConfiguration", "QueueConfigurationDeprecated", - "TopicConfigurationDeprecated", "NotificationConfigurationDeprecated", - "RequestPaymentConfiguration", "PutObjectLockConfigurationRequestAlias", - "GetObjectLockConfigurationResultAlias", "ObjectLockConfigurationAlias", - "ObjectLockRuleAlias", "DefaultRetentionAlias", "ObjectLockRetentionAlias" - ) - ); - /** * Returns the hardcoded collision resolution for a shape, if one exists. */ diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 2ef81b2ca3c..a3d1452b7fc 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -8,6 +8,9 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.transform.ModelTransformer; +import java.util.Map; /** * S3 (and S3-CRT, which shares the S3 model) parity with the legacy C2J @@ -32,6 +35,20 @@ private static Model apply(Model model, ServiceShape service) { } // Sub-transforms are chained here by later tasks, e.g.: // return normalizeReplicationStatus(expandBucketLocationConstraint(... (model) ...)); - return model; + return renameCopyObjectResult(model); + } + + private static Model renameCopyObjectResult(Model model) { + String ns = "com.amazonaws.s3"; + ShapeId oldId = ShapeId.fromParts(ns, "CopyObjectResult"); + ShapeId newId = ShapeId.fromParts(ns, "CopyObjectResultDetails"); + if (model.getShape(oldId).isEmpty()) { + return model; // source absent: nothing to rename. + } + if (model.getShape(newId).isPresent()) { + throw new IllegalStateException("S3 collision: '" + newId + "' already exists; cannot " + + "rename '" + oldId + "' onto it."); + } + return ModelTransformer.create().renameShapes(model, Map.of(oldId, newId)); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index c09404da4ef..6fe6898c3a4 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -49,4 +49,28 @@ void noOpForS3WhenNothingToDo() { assertNotNull(out); assertTrue(out.getShape(ShapeId.from(NS + "#AmazonS3")).isPresent()); } + + @Test + void renamesCopyObjectResultToDetails() { + ServiceShape svc = s3Service("S3"); + StructureShape copyResult = StructureShape.builder().id(NS + "#CopyObjectResult") + .addMember("ETag", ShapeId.from("smithy.api#String")).build(); + Model m = modelWith(svc, copyResult); + Model out = S3Transforms.asTransform().apply(m, svc); + assertTrue(out.getShape(ShapeId.from(NS + "#CopyObjectResultDetails")).isPresent(), + "renamed to CopyObjectResultDetails"); + assertFalse(out.getShape(ShapeId.from(NS + "#CopyObjectResult")).isPresent(), + "old name gone"); + } + + @Test + void copyObjectResultRename_throwsOnCollision() { + ServiceShape svc = s3Service("S3"); + StructureShape copyResult = StructureShape.builder().id(NS + "#CopyObjectResult") + .addMember("ETag", ShapeId.from("smithy.api#String")).build(); + StructureShape details = StructureShape.builder().id(NS + "#CopyObjectResultDetails") + .addMember("Other", ShapeId.from("smithy.api#String")).build(); + Model m = modelWith(svc, copyResult, details); + assertThrows(IllegalStateException.class, () -> S3Transforms.asTransform().apply(m, svc)); + } } From 46e87f05dbaf675e7817743a67148f8f269a1497 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 11:01:30 -0400 Subject: [PATCH 17/44] Smithy: remove dead ShapeUtil collision-resolution helpers (folded into S3Transforms) --- .../generators/ShapeUtil.java | 35 ------------ .../model/ShapeUtilExtensionsTest.java | 54 ------------------- 2 files changed, 89 deletions(-) delete mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java index ccfc16f6b05..5b5c391d8e6 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ShapeUtil.java @@ -64,41 +64,6 @@ public class ShapeUtil { "cloudfront", "2020_05_31" ); - /** - * Hardcoded shape rename collision resolutions from C2J. - * These shapes had name collisions with operation result wrappers in C2J - * and were given specific alternative names. - * Map: service-name -> Map of original-shape-name -> resolved-name - */ - private static final Map> HARDCODED_COLLISION_RESOLUTIONS = Map.of( - // accessanalyzer GeneratedPolicyResult->GeneratedPolicyResults is handled by - // AccessAnalyzerTransforms (a model transform), not this render-time map. - // cloudsearchdomain SearchResult->SearchResultDetails is dead: the current model has no - // colliding SearchResult shape. The s3 entry stays for the deferred S3 transform work. - "s3", Map.of("CopyObjectResult", "CopyObjectResultDetails") - ); - - /** - * Returns the hardcoded collision resolution for a shape, if one exists. - */ - public static Optional getHardcodedResolution(String smithyServiceName, String shapeName) { - Map serviceResolutions = HARDCODED_COLLISION_RESOLUTIONS.get(smithyServiceName); - if (serviceResolutions == null) return Optional.empty(); - return Optional.ofNullable(serviceResolutions.get(shapeName)); - } - - /** - * Returns the C++ class name for a shape, applying collision renames and numeric prefix rules. - */ - public static String getShapeCppName(String shapeName, String smithyServiceName) { - Optional resolved = getHardcodedResolution(smithyServiceName, shapeName); - if (resolved.isPresent()) return resolved.get(); - if (!shapeName.isEmpty() && Character.isDigit(shapeName.charAt(0))) { - return "The" + shapeName; - } - return shapeName; - } - /** * C2J/Smithy model mismatches: tokens that are integers in C2J but strings in Smithy. * diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java deleted file mode 100644 index 5daec8ce043..00000000000 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ -package com.amazonaws.util.awsclientsmithygenerator.generators.model; - -import com.amazonaws.util.awsclientsmithygenerator.generators.ShapeUtil; -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; - -class ShapeUtilExtensionsTest { - - @Test - void hardcodedCollisionResolution_s3CopyObjectResult() { - assertEquals(Optional.of("CopyObjectResultDetails"), - ShapeUtil.getHardcodedResolution("s3", "CopyObjectResult")); - } - - - @Test - void hardcodedCollisionResolution_noMatch_returnsEmpty() { - assertTrue(ShapeUtil.getHardcodedResolution("kinesis", "SomeShape").isEmpty()); - } - - @Test - void hardcodedCollisionResolution_medialive_noEntry() { - assertTrue(ShapeUtil.getHardcodedResolution("medialive", "BatchUpdateScheduleResult").isEmpty()); - } - - @Test - void shapeCppName_numericPrefix() { - assertEquals("The1stShape", ShapeUtil.getShapeCppName("1stShape", "someservice")); - } - - @Test - void shapeCppName_normalName_unchanged() { - assertEquals("MyShape", ShapeUtil.getShapeCppName("MyShape", "someservice")); - } - - @Test - void shapeCppName_medialive_noOverride_returnsUnchanged() { - assertEquals("BatchUpdateScheduleResult", - ShapeUtil.getShapeCppName("BatchUpdateScheduleResult", "medialive")); - } - - @Test - void shapeCppName_withHardcodedResolution_s3CopyObjectResult() { - assertEquals("CopyObjectResultDetails", - ShapeUtil.getShapeCppName("CopyObjectResult", "s3")); - } -} From fe84821587f9d2a744ea54df88f4cf5ded3ad5f6 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 11:08:04 -0400 Subject: [PATCH 18/44] Smithy: S3Transforms adds Expires/ExpiresString backward-compat member --- .../model/transforms/S3Transforms.java | 59 ++++++++++++++++++- .../model/transforms/S3TransformsTest.java | 29 +++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index a3d1452b7fc..c0bb3e3797f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -7,9 +7,16 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.transform.ModelTransformer; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -35,7 +42,7 @@ private static Model apply(Model model, ServiceShape service) { } // Sub-transforms are chained here by later tasks, e.g.: // return normalizeReplicationStatus(expandBucketLocationConstraint(... (model) ...)); - return renameCopyObjectResult(model); + return addExpiresCustomization(renameCopyObjectResult(model)); } private static Model renameCopyObjectResult(Model model) { @@ -51,4 +58,54 @@ private static Model renameCopyObjectResult(Model model) { } return ModelTransformer.create().renameShapes(model, Map.of(oldId, newId)); } + + private static final String EXPIRES_DEPRECATION = + "Deprecated: Please use ExpiresString instead. " + System.lineSeparator() + " * "; + + private static Model addExpiresCustomization(Model model) { + String ns = "com.amazonaws.s3"; + ShapeId expiresStringId = ShapeId.fromParts(ns, "ExpiresString"); + List withExpires = model.shapes(StructureShape.class) + .filter(s -> s.getMember("Expires").isPresent()) + .toList(); + if (withExpires.isEmpty()) { + return model; // no Expires anywhere: nothing to do. + } + List replacements = new ArrayList<>(); + // Inject the ExpiresString string shape once (idempotent: skip if present). + if (model.getShape(expiresStringId).isEmpty()) { + replacements.add(StringShape.builder().id(expiresStringId).build()); + } + for (StructureShape struct : withExpires) { + if (struct.getMember("ExpiresString").isPresent()) { + continue; // already customized (idempotent). + } + MemberShape expires = struct.getAllMembers().get("Expires"); + StructureShape.Builder b = StructureShape.builder().id(struct.getId()); + struct.getAllTraits().values().forEach(b::addTrait); + for (MemberShape m : struct.getAllMembers().values()) { + if (m.getMemberName().equals("Expires")) { + // Rewrite Expires' documentation to prepend the deprecation note. + String existingDoc = m.getTrait(DocumentationTrait.class) + .map(DocumentationTrait::getValue).orElse(""); + b.addMember("Expires", m.getTarget(), mb -> { + m.getAllTraits().values().forEach(mb::addTrait); + if (!existingDoc.toLowerCase().contains("deprecated")) { + mb.addTrait(new DocumentationTrait(EXPIRES_DEPRECATION + existingDoc)); + } + }); + } else { + b.addMember(m.getMemberName(), m.getTarget(), + mb -> m.getAllTraits().values().forEach(mb::addTrait)); + } + } + // Add ExpiresString cloning Expires' traits (so it reads the same header), retargeted. + b.addMember("ExpiresString", expiresStringId, + mb -> expires.getAllTraits().values().stream() + .filter(t -> !(t instanceof DocumentationTrait)) + .forEach(mb::addTrait)); + replacements.add(b.build()); + } + return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 6fe6898c3a4..6ff99f1ffb5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StructureShape; @@ -73,4 +74,32 @@ void copyObjectResultRename_throwsOnCollision() { Model m = modelWith(svc, copyResult, details); assertThrows(IllegalStateException.class, () -> S3Transforms.asTransform().apply(m, svc)); } + + @Test + void addsExpiresStringMemberAndDeprecatesExpires() { + ServiceShape svc = s3Service("S3"); + software.amazon.smithy.model.shapes.TimestampShape expires = + software.amazon.smithy.model.shapes.TimestampShape.builder() + .id(NS + "#Expires").build(); + StructureShape getObjectOutput = StructureShape.builder().id(NS + "#GetObjectOutput") + .addMember("Expires", expires.getId(), b -> b + .addTrait(new software.amazon.smithy.model.traits.HttpHeaderTrait("Expires")) + .addTrait(new software.amazon.smithy.model.traits.DocumentationTrait("The date and time at which the object is no longer cacheable."))) + .build(); + Model m = modelWith(svc, expires, getObjectOutput); + Model out = S3Transforms.asTransform().apply(m, svc); + + assertTrue(out.getShape(ShapeId.from(NS + "#ExpiresString")).isPresent(), + "ExpiresString string shape injected"); + StructureShape outShape = out.expectShape(ShapeId.from(NS + "#GetObjectOutput"), StructureShape.class); + MemberShape expiresString = outShape.getMember("ExpiresString").orElseThrow(); + assertEquals(NS + "#ExpiresString", expiresString.getTarget().toString()); + assertEquals("Expires", + expiresString.expectTrait(software.amazon.smithy.model.traits.HttpHeaderTrait.class).getValue(), + "ExpiresString reads the same Expires header"); + MemberShape expiresMember = outShape.getMember("Expires").orElseThrow(); + assertTrue(expiresMember.expectTrait(software.amazon.smithy.model.traits.DocumentationTrait.class) + .getValue().startsWith("Deprecated: Please use ExpiresString instead."), + "Expires member carries the deprecation note"); + } } From 56a733b4e86e93536dfe18c6d77b4015afb960b3 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 11:12:59 -0400 Subject: [PATCH 19/44] Smithy: scope S3 ExpiresString to outputs and retype Expires to timestamp --- .../model/transforms/S3Transforms.java | 30 ++++++- .../model/transforms/S3TransformsTest.java | 83 +++++++++++++++---- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index c0bb3e3797f..778e89da2f9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -7,17 +7,22 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.transform.ModelTransformer; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * S3 (and S3-CRT, which shares the S3 model) parity with the legacy C2J @@ -42,7 +47,7 @@ private static Model apply(Model model, ServiceShape service) { } // Sub-transforms are chained here by later tasks, e.g.: // return normalizeReplicationStatus(expandBucketLocationConstraint(... (model) ...)); - return addExpiresCustomization(renameCopyObjectResult(model)); + return addExpiresCustomization(renameCopyObjectResult(model), service); } private static Model renameCopyObjectResult(Model model) { @@ -62,14 +67,22 @@ private static Model renameCopyObjectResult(Model model) { private static final String EXPIRES_DEPRECATION = "Deprecated: Please use ExpiresString instead. " + System.lineSeparator() + " * "; - private static Model addExpiresCustomization(Model model) { + private static Model addExpiresCustomization(Model model, ServiceShape service) { String ns = "com.amazonaws.s3"; + // C2J renders Expires as a timestamp on every shape that carries it, though the current + // Smithy model types it as a string. Retype the shape itself before scoping ExpiresString. + model = retypeExpiresToTimestamp(model, ns); ShapeId expiresStringId = ShapeId.fromParts(ns, "ExpiresString"); + // ExpiresString (and the deprecation note) live only on operation-output structures. + Set outputShapes = TopDownIndex.of(model).getContainedOperations(service).stream() + .map(OperationShape::getOutputShape) + .collect(Collectors.toSet()); List withExpires = model.shapes(StructureShape.class) + .filter(s -> outputShapes.contains(s.getId())) .filter(s -> s.getMember("Expires").isPresent()) .toList(); if (withExpires.isEmpty()) { - return model; // no Expires anywhere: nothing to do. + return model; // no output shape carries Expires: nothing more to do. } List replacements = new ArrayList<>(); // Inject the ExpiresString string shape once (idempotent: skip if present). @@ -108,4 +121,15 @@ private static Model addExpiresCustomization(Model model) { } return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } + + private static Model retypeExpiresToTimestamp(Model model, String ns) { + ShapeId expiresId = ShapeId.fromParts(ns, "Expires"); + Shape existing = model.getShape(expiresId).orElse(null); + if (existing == null || existing instanceof TimestampShape) { + return model; // absent or already a timestamp: nothing to retype. + } + TimestampShape.Builder b = TimestampShape.builder().id(expiresId); + existing.getAllTraits().values().forEach(b::addTrait); + return model.toBuilder().addShape(b.build()).build(); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 6ff99f1ffb5..c0875701755 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -8,9 +8,15 @@ import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.HttpHeaderTrait; import static org.junit.jupiter.api.Assertions.*; @@ -75,31 +81,80 @@ void copyObjectResultRename_throwsOnCollision() { assertThrows(IllegalStateException.class, () -> S3Transforms.asTransform().apply(m, svc)); } - @Test - void addsExpiresStringMemberAndDeprecatesExpires() { - ServiceShape svc = s3Service("S3"); - software.amazon.smithy.model.shapes.TimestampShape expires = - software.amazon.smithy.model.shapes.TimestampShape.builder() - .id(NS + "#Expires").build(); - StructureShape getObjectOutput = StructureShape.builder().id(NS + "#GetObjectOutput") + /** + * Builds an S3 model with a single PutObject-style operation whose input and output both + * carry an {@code Expires} member (initially a {@code string}, matching the current model), + * mirroring the operation-wiring pattern in {@code AccessAnalyzerTransformsTest}. + */ + private static Model expiresModel() { + Shape expires = StringShape.builder().id(NS + "#Expires").build(); + StructureShape input = StructureShape.builder().id(NS + "#PutObjectRequest") .addMember("Expires", expires.getId(), b -> b - .addTrait(new software.amazon.smithy.model.traits.HttpHeaderTrait("Expires")) - .addTrait(new software.amazon.smithy.model.traits.DocumentationTrait("The date and time at which the object is no longer cacheable."))) + .addTrait(new HttpHeaderTrait("Expires")) + .addTrait(new DocumentationTrait("The date and time at which the object is no longer cacheable."))) .build(); - Model m = modelWith(svc, expires, getObjectOutput); - Model out = S3Transforms.asTransform().apply(m, svc); + StructureShape output = StructureShape.builder().id(NS + "#GetObjectOutput") + .addMember("Expires", expires.getId(), b -> b + .addTrait(new HttpHeaderTrait("Expires")) + .addTrait(new DocumentationTrait("The date and time at which the object is no longer cacheable."))) + .build(); + OperationShape op = OperationShape.builder().id(NS + "#GetObject") + .input(input.getId()).output(output.getId()).build(); + ServiceShape svc = ServiceShape.builder().id(NS + "#AmazonS3").version("2006-03-01") + .addTrait(ServiceTrait.builder().sdkId("S3").arnNamespace("s3") + .cloudFormationName("S3").cloudTrailEventSource("s3.amazonaws.com").build()) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(expires, input, output, op, svc).assemble().unwrap(); + } + + private static ServiceShape expiresService(Model m) { + return m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); + } + + @Test + void retypesExpiresShapeToTimestamp() { + Model m = expiresModel(); + assertTrue(m.expectShape(ShapeId.from(NS + "#Expires")).isStringShape(), + "precondition: Expires starts as a string"); + Model out = S3Transforms.asTransform().apply(m, expiresService(m)); + assertTrue(out.expectShape(ShapeId.from(NS + "#Expires")) instanceof TimestampShape, + "Expires retyped to a timestamp shape"); + // Both input and output Expires members now target the timestamp. + StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); + StructureShape output = out.expectShape(ShapeId.from(NS + "#GetObjectOutput"), StructureShape.class); + assertTrue(out.expectShape(input.getMember("Expires").orElseThrow().getTarget()) instanceof TimestampShape); + assertTrue(out.expectShape(output.getMember("Expires").orElseThrow().getTarget()) instanceof TimestampShape); + } + + @Test + void addsExpiresStringToOutputAndDeprecatesExpires() { + Model m = expiresModel(); + Model out = S3Transforms.asTransform().apply(m, expiresService(m)); assertTrue(out.getShape(ShapeId.from(NS + "#ExpiresString")).isPresent(), "ExpiresString string shape injected"); StructureShape outShape = out.expectShape(ShapeId.from(NS + "#GetObjectOutput"), StructureShape.class); MemberShape expiresString = outShape.getMember("ExpiresString").orElseThrow(); assertEquals(NS + "#ExpiresString", expiresString.getTarget().toString()); - assertEquals("Expires", - expiresString.expectTrait(software.amazon.smithy.model.traits.HttpHeaderTrait.class).getValue(), + assertEquals("Expires", expiresString.expectTrait(HttpHeaderTrait.class).getValue(), "ExpiresString reads the same Expires header"); MemberShape expiresMember = outShape.getMember("Expires").orElseThrow(); - assertTrue(expiresMember.expectTrait(software.amazon.smithy.model.traits.DocumentationTrait.class) + assertTrue(expiresMember.expectTrait(DocumentationTrait.class) .getValue().startsWith("Deprecated: Please use ExpiresString instead."), "Expires member carries the deprecation note"); } + + @Test + void doesNotAddExpiresStringToInput() { + Model m = expiresModel(); + Model out = S3Transforms.asTransform().apply(m, expiresService(m)); + + StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); + assertFalse(input.getMember("ExpiresString").isPresent(), + "input shape must not gain ExpiresString"); + MemberShape inputExpires = input.getMember("Expires").orElseThrow(); + assertFalse(inputExpires.getTrait(DocumentationTrait.class) + .map(DocumentationTrait::getValue).orElse("").startsWith("Deprecated:"), + "input Expires must not carry the deprecation note"); + } } From 00b805e806bac487c14197ab42f8da0426bfef18 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 11:17:37 -0400 Subject: [PATCH 20/44] Smithy: S3Transforms injects GetObject Id2/RequestId header members --- .../model/transforms/S3Transforms.java | 31 ++++++++++++++++++- .../model/transforms/S3TransformsTest.java | 20 ++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 778e89da2f9..c1236983a56 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -17,10 +17,12 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.HttpHeaderTrait; import software.amazon.smithy.model.transform.ModelTransformer; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -47,7 +49,34 @@ private static Model apply(Model model, ServiceShape service) { } // Sub-transforms are chained here by later tasks, e.g.: // return normalizeReplicationStatus(expandBucketLocationConstraint(... (model) ...)); - return addExpiresCustomization(renameCopyObjectResult(model), service); + return hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service)); + } + + private static Model hackGetObjectResult(Model model) { + String ns = "com.amazonaws.s3"; + ShapeId outputId = ShapeId.fromParts(ns, "GetObjectOutput"); + Optional outputOpt = model.getShape(outputId).flatMap(Shape::asStructureShape); + if (outputOpt.isEmpty()) { + return model; // no GetObjectOutput: nothing to do. + } + StructureShape output = outputOpt.get(); + if (output.getMember("Id2").isPresent() && output.getMember("RequestId").isPresent()) { + return model; // already injected (idempotent) — or upstream added them. + } + ShapeId id2ShapeId = ShapeId.fromParts(ns, "ObjectId2"); + ShapeId reqIdShapeId = ShapeId.fromParts(ns, "ObjectRequestId"); + StringShape id2Shape = StringShape.builder().id(id2ShapeId).build(); + StringShape reqIdShape = StringShape.builder().id(reqIdShapeId).build(); + + StructureShape.Builder b = StructureShape.builder().id(output.getId()); + output.getAllTraits().values().forEach(b::addTrait); + output.getAllMembers().values().forEach(m -> + b.addMember(m.getMemberName(), m.getTarget(), + mb -> m.getAllTraits().values().forEach(mb::addTrait))); + b.addMember("Id2", id2ShapeId, mb -> mb.addTrait(new HttpHeaderTrait("x-amz-id-2"))); + b.addMember("RequestId", reqIdShapeId, mb -> mb.addTrait(new HttpHeaderTrait("x-amz-request-id"))); + + return model.toBuilder().addShapes(id2Shape, reqIdShape, b.build()).build(); } private static Model renameCopyObjectResult(Model model) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index c0875701755..71959c15ae6 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -157,4 +157,24 @@ void doesNotAddExpiresStringToInput() { .map(DocumentationTrait::getValue).orElse("").startsWith("Deprecated:"), "input Expires must not carry the deprecation note"); } + + @Test + void injectsGetObjectId2AndRequestId() { + ServiceShape svc = s3Service("S3"); + StructureShape getObjectOutput = StructureShape.builder().id(NS + "#GetObjectOutput") + .addMember("ETag", ShapeId.from("smithy.api#String")).build(); + Model m = modelWith(svc, getObjectOutput); + Model out = S3Transforms.asTransform().apply(m, svc); + + assertTrue(out.getShape(ShapeId.from(NS + "#ObjectId2")).isPresent()); + assertTrue(out.getShape(ShapeId.from(NS + "#ObjectRequestId")).isPresent()); + StructureShape outShape = out.expectShape(ShapeId.from(NS + "#GetObjectOutput"), StructureShape.class); + MemberShape id2 = outShape.getMember("Id2").orElseThrow(); + assertEquals(NS + "#ObjectId2", id2.getTarget().toString()); + assertEquals("x-amz-id-2", + id2.expectTrait(software.amazon.smithy.model.traits.HttpHeaderTrait.class).getValue()); + MemberShape reqId = outShape.getMember("RequestId").orElseThrow(); + assertEquals("x-amz-request-id", + reqId.expectTrait(software.amazon.smithy.model.traits.HttpHeaderTrait.class).getValue()); + } } From a835e8e176e6b26a28b5d718cfade96dd5893d59 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 11:25:13 -0400 Subject: [PATCH 21/44] Smithy: S3Transforms appends missing BucketLocationConstraint regions --- .../model/transforms/S3Transforms.java | 30 +++++++- .../model/transforms/TransformSupport.java | 68 +++++++++++++++++++ .../model/transforms/S3TransformsTest.java | 43 ++++++++++++ .../transforms/TransformSupportTest.java | 55 +++++++++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index c1236983a56..25e222bb731 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -17,9 +17,11 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.HttpHeaderTrait; import software.amazon.smithy.model.transform.ModelTransformer; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -49,7 +51,33 @@ private static Model apply(Model model, ServiceShape service) { } // Sub-transforms are chained here by later tasks, e.g.: // return normalizeReplicationStatus(expandBucketLocationConstraint(... (model) ...)); - return hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service)); + return expandBucketLocationConstraint( + hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service))); + } + + // Confirmed delta at implementation time against the live s3.json BucketLocationConstraint enum; + // the other 18 C2J regions are already present upstream. + private static final List MISSING_REGIONS = List.of("us-east-1", "us-iso-west-1"); + + private static Model expandBucketLocationConstraint(Model model) { + Optional enumShape = model.shapes() + .filter(s -> "BucketLocationConstraint".equals(s.getId().getName())) + .filter(s -> s.isEnumShape() || s.hasTrait(EnumTrait.class)) + .findFirst(); + if (enumShape.isEmpty()) { + return model; + } + return TransformSupport.appendEnumValues(enumShape.get(), regionNameValueMap()) + .map(updated -> model.toBuilder().addShape(updated).build()) + .orElse(model); + } + + private static Map regionNameValueMap() { + LinkedHashMap map = new LinkedHashMap<>(); + for (String region : MISSING_REGIONS) { + map.put(region.replace('-', '_'), region); + } + return map; } private static Model hackGetObjectResult(Model model) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java index 67e31b6a511..54c585aa68c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java @@ -18,8 +18,11 @@ import software.amazon.smithy.model.traits.XmlNameTrait; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; /** * Shared helpers for per-service model transforms. @@ -84,6 +87,71 @@ static Optional appendValues(Shape enumShape, List values) { .build()); } + /** + * Appends {@code name -> value} enum entries, allowing wire values that are not + * identifier-safe (e.g. region strings containing {@code '-'} such as {@code us-east-1}). This is + * the name/value counterpart of {@link #appendValues(Shape, List)}, which requires the wire value + * to double as the member name. + * + *

Each map key is the Smithy member name and MUST be identifier-safe (matching + * {@code [A-Za-z_][A-Za-z0-9_]*}); each map value is the wire value and may be arbitrary. For a + * Smithy 2.0 {@code EnumShape} the name becomes the member name and the value the + * {@code @enumValue} via {@code builder.addMember(name, value)}; for a legacy {@code @enum} + * {@code StringShape} only the wire value is recorded (matching C2J, which keys the enum off the + * wire value and derives the constant name by sanitizing it). + * + *

Idempotent: entries whose wire value already exists are skipped; if every value is already + * present the shape is returned unchanged as {@link Optional#empty()}. + * + * @param enumShape the enum shape to append to + * @param nameToValue ordered member-name to wire-value entries to append + * @return the updated shape, or {@link Optional#empty()} if all values are already present + * @throws IllegalArgumentException if any member name is not identifier-safe + */ + static Optional appendEnumValues(Shape enumShape, Map nameToValue) { + for (String name : nameToValue.keySet()) { + if (name == null || !name.matches(IDENTIFIER_PATTERN)) { + throw new IllegalArgumentException( + "Enum member name \"" + name + "\" for shape " + enumShape.getId() + + " is not an identifier-safe enum member name (must match " + + IDENTIFIER_PATTERN + ")"); + } + } + List existing = existingWireValues(enumShape); + LinkedHashMap toAdd = new LinkedHashMap<>(); + nameToValue.forEach((name, value) -> { + if (!existing.contains(value)) { + toAdd.put(name, value); + } + }); + if (toAdd.isEmpty()) { + return Optional.empty(); + } + if (enumShape.isEnumShape()) { + EnumShape.Builder builder = enumShape.asEnumShape().get().toBuilder(); + toAdd.forEach(builder::addMember); + return Optional.of(builder.build()); + } + EnumTrait existingTrait = enumShape.expectTrait(EnumTrait.class); + EnumTrait.Builder traitBuilder = EnumTrait.builder(); + existingTrait.getValues().forEach(traitBuilder::addEnum); + toAdd.values().forEach(value -> + traitBuilder.addEnum(EnumDefinition.builder().value(value).build())); + return Optional.of(enumShape.asStringShape().get().toBuilder() + .addTrait(traitBuilder.build()) + .build()); + } + + /** The current wire values of an enum shape (Smithy 2.0 {@code EnumShape} or legacy {@code @enum}). */ + private static List existingWireValues(Shape enumShape) { + if (enumShape.isEnumShape()) { + return new ArrayList<>(enumShape.asEnumShape().get().getEnumValues().values()); + } + return enumShape.expectTrait(EnumTrait.class).getValues().stream() + .map(EnumDefinition::getValue) + .collect(Collectors.toList()); + } + /** * Returns a copy of {@code struct} with member {@code oldName} renamed to {@code newName}, * preserving member declaration order and copying all traits onto the renamed member. Returns diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 71959c15ae6..a12bf8a7880 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -158,6 +158,49 @@ void doesNotAddExpiresStringToInput() { "input Expires must not carry the deprecation note"); } + @Test + void appendsMissingBucketLocationConstraintRegions() { + ServiceShape svc = s3Service("S3"); + // Model BucketLocationConstraint as an EnumShape with an existing region. + software.amazon.smithy.model.shapes.EnumShape enumShape = + software.amazon.smithy.model.shapes.EnumShape.builder() + .id(NS + "#BucketLocationConstraint") + .addMember("us_west_2", "us-west-2") + .build(); + Model m = modelWith(svc, enumShape); + Model out = S3Transforms.asTransform().apply(m, svc); + + software.amazon.smithy.model.shapes.EnumShape result = out.expectShape( + ShapeId.from(NS + "#BucketLocationConstraint"), + software.amazon.smithy.model.shapes.EnumShape.class); + // EnumRenderer.getEnumValues() sanitizes '-' to '_', so assert on the raw wire values here. + java.util.Collection wireValues = result.getEnumValues().values(); + assertTrue(wireValues.contains("us-east-1"), "us-east-1 appended"); + assertTrue(wireValues.contains("us-iso-west-1"), "us-iso-west-1 appended"); + assertTrue(wireValues.contains("us-west-2"), "existing value preserved"); + // Member names are identifier-safe, matching the existing model form (hyphens -> underscores). + assertTrue(result.getAllMembers().containsKey("us_east_1"), "identifier-safe member name"); + assertTrue(result.getAllMembers().containsKey("us_iso_west_1"), "identifier-safe member name"); + } + + @Test + void bucketLocationConstraintExpansionIsIdempotent() { + ServiceShape svc = s3Service("S3"); + software.amazon.smithy.model.shapes.EnumShape enumShape = + software.amazon.smithy.model.shapes.EnumShape.builder() + .id(NS + "#BucketLocationConstraint") + .addMember("us_west_2", "us-west-2") + .build(); + Model m = modelWith(svc, enumShape); + Model once = S3Transforms.asTransform().apply(m, svc); + Model twice = S3Transforms.asTransform().apply(once, svc); + software.amazon.smithy.model.shapes.EnumShape result = twice.expectShape( + ShapeId.from(NS + "#BucketLocationConstraint"), + software.amazon.smithy.model.shapes.EnumShape.class); + long usEast1 = result.getEnumValues().values().stream().filter("us-east-1"::equals).count(); + assertEquals(1, usEast1, "re-applying must not duplicate appended values"); + } + @Test void injectsGetObjectId2AndRequestId() { ServiceShape svc = s3Service("S3"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java index e5d23270cb4..f16b7f325c8 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java @@ -7,14 +7,21 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import org.junit.jupiter.api.Test; import software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait; +import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.JsonNameTrait; import software.amazon.smithy.model.traits.XmlNameTrait; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; @@ -123,6 +130,54 @@ void renameMember_cborProtocol_throws_noWireNameTrait() { () -> TransformSupport.renameMember(s, "body", "requestBody", Protocol.CBOR)); } + private static LinkedHashMap map(String name, String value) { + LinkedHashMap m = new LinkedHashMap<>(); + m.put(name, value); + return m; + } + + @Test + void appendEnumValues_enumShape_appendsHyphenatedWireValue() { + EnumShape shape = EnumShape.builder().id("com.example#Region") + .addMember("us_west_2", "us-west-2").build(); + Shape out = TransformSupport.appendEnumValues(shape, map("us_east_1", "us-east-1")) + .orElseThrow(); + EnumShape e = out.asEnumShape().orElseThrow(); + assertTrue(e.getEnumValues().values().contains("us-east-1"), "hyphenated value appended"); + assertTrue(e.getEnumValues().values().contains("us-west-2"), "existing value preserved"); + assertTrue(e.getAllMembers().containsKey("us_east_1"), "identifier-safe member name"); + } + + @Test + void appendEnumValues_idempotentSkipsExistingValue() { + EnumShape shape = EnumShape.builder().id("com.example#Region") + .addMember("us_east_1", "us-east-1").build(); + assertTrue(TransformSupport.appendEnumValues(shape, map("us_east_1", "us-east-1")).isEmpty(), + "already-present wire value must be skipped"); + } + + @Test + void appendEnumValues_legacyEnumTrait_appendsValue() { + StringShape shape = StringShape.builder().id("com.example#Region") + .addTrait(EnumTrait.builder() + .addEnum(EnumDefinition.builder().value("us-west-2").build()).build()) + .build(); + Shape out = TransformSupport.appendEnumValues(shape, map("us_east_1", "us-east-1")) + .orElseThrow(); + List values = out.expectTrait(EnumTrait.class).getValues().stream() + .map(EnumDefinition::getValue).collect(Collectors.toList()); + assertTrue(values.contains("us-west-2"), "existing value preserved"); + assertTrue(values.contains("us-east-1"), "hyphenated value appended"); + } + + @Test + void appendEnumValues_nonIdentifierMemberName_throws() { + EnumShape shape = EnumShape.builder().id("com.example#Region") + .addMember("us_west_2", "us-west-2").build(); + assertThrows(IllegalArgumentException.class, + () -> TransformSupport.appendEnumValues(shape, map("us-east-1", "us-east-1"))); + } + @Test void renameMember_existingJsonName_isNotOverridden() { StructureShape s = StructureShape.builder().id("com.example#Req") From d78656fb056c63d029ff7822992839d945c67e88 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 11:30:54 -0400 Subject: [PATCH 22/44] Smithy: S3Transforms normalizes ReplicationStatus COMPLETE to COMPLETED --- .../model/transforms/S3Transforms.java | 40 +++++++++++++++++-- .../model/transforms/S3TransformsTest.java | 21 ++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 25e222bb731..babe2985262 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -5,9 +5,11 @@ package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.EnumRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -49,10 +51,40 @@ private static Model apply(Model model, ServiceShape service) { if (!"s3".equals(name) && !"s3-crt".equals(name)) { return model; } - // Sub-transforms are chained here by later tasks, e.g.: - // return normalizeReplicationStatus(expandBucketLocationConstraint(... (model) ...)); - return expandBucketLocationConstraint( - hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service))); + return normalizeReplicationStatus(expandBucketLocationConstraint( + hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service)))); + } + + // C2J collapses the model's split COMPLETE/COMPLETED ReplicationStatus values into a single + // COMPLETED constant. Drop the extra COMPLETED and rewrite COMPLETE to COMPLETED, preserving + // the remaining member order. Both remove and rewrite means we rebuild the enum explicitly. + private static Model normalizeReplicationStatus(Model model) { + Optional shapeOpt = model.shapes() + .filter(s -> s.getId().getMember().isEmpty()) + .filter(s -> "ReplicationStatus".equals(s.getId().getName())) + .findFirst(); + if (shapeOpt.isEmpty()) { + return model; // shape absent: nothing to normalize. + } + Shape found = shapeOpt.get(); + if (!found.isEnumShape()) { + throw new IllegalStateException("S3: expected 'ReplicationStatus' to be an EnumShape but " + + "found " + found.getType() + "; legacy @enum handling is unimplemented."); + } + EnumShape shape = found.asEnumShape().get(); + List values = EnumRenderer.getEnumValues(shape); + if (!values.contains("COMPLETE")) { + return model; // upstream already normalized. + } + EnumShape.Builder b = EnumShape.builder().id(shape.getId()); + shape.getAllTraits().values().forEach(b::addTrait); + for (String v : values) { + if (!"COMPLETED".equals(v)) { + String value = "COMPLETE".equals(v) ? "COMPLETED" : v; + b.addMember(value, value); + } + } + return model.toBuilder().addShape(b.build()).build(); } // Confirmed delta at implementation time against the live s3.json BucketLocationConstraint enum; diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index a12bf8a7880..677d6181b13 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -201,6 +201,27 @@ void bucketLocationConstraintExpansionIsIdempotent() { assertEquals(1, usEast1, "re-applying must not duplicate appended values"); } + @Test + void normalizesReplicationStatusCompleteToCompleted() { + ServiceShape svc = s3Service("S3"); + software.amazon.smithy.model.shapes.EnumShape enumShape = + software.amazon.smithy.model.shapes.EnumShape.builder() + .id(NS + "#ReplicationStatus") + .addMember("COMPLETE", "COMPLETE") + .addMember("PENDING", "PENDING") + .addMember("FAILED", "FAILED") + .addMember("REPLICA", "REPLICA") + .addMember("COMPLETED", "COMPLETED") + .build(); + Model m = modelWith(svc, enumShape); + Model out = S3Transforms.asTransform().apply(m, svc); + + java.util.List values = com.amazonaws.util.awsclientsmithygenerator.generators.model + .EnumRenderer.getEnumValues(out.expectShape(ShapeId.from(NS + "#ReplicationStatus"))); + assertEquals(java.util.List.of("COMPLETED", "PENDING", "FAILED", "REPLICA"), values, + "COMPLETE rewritten to COMPLETED, duplicate removed, order preserved"); + } + @Test void injectsGetObjectId2AndRequestId() { ServiceShape svc = s3Service("S3"); From 6bb76ee21b73b8451d61a249657e449333b157c8 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 11:40:12 -0400 Subject: [PATCH 23/44] Smithy: record S3 serde-phased customizations as deferred parity deltas --- docs/superpowers/plans/parity-deltas.md | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/superpowers/plans/parity-deltas.md diff --git a/docs/superpowers/plans/parity-deltas.md b/docs/superpowers/plans/parity-deltas.md new file mode 100644 index 00000000000..a7ba6d78859 --- /dev/null +++ b/docs/superpowers/plans/parity-deltas.md @@ -0,0 +1,88 @@ +# Smithy Per-Service Model Parity — Documented Deltas + +Reviewed, accepted differences between Smithy-generated and C2J-generated `model::` +files. Anything not listed here must be resolved (empty diff) before a transform is "done". + +## Global-baseline deltas (not service-specific) +- **Stubbed payload serde (global):** Smithy emits empty serde bodies (e.g. `SerializePayload() const { return {}; }`, `OutputToStream(...) {}`) where C2J emits full serialization. Affects every shape; not service-specific. Pending serde implementation in the model plugin. +- **Doc-comment reflow (global):** Smithy wraps member documentation comments differently from C2J. Cosmetic; affects many members across services. + + +- **Pagination-traits files (global):** Smithy emits `PaginationTraits.h` under model/ that the C2J model tree doesn't; separate pagination plugin output, pre-existing. +- **ResponseMetadata standalone file (query/ec2, global):** C2J emits a standalone `ResponseMetadata.{h,cpp}`; the Smithy plugin injects ResponseMetadata via GlobalTransforms but doesn't emit it as a standalone sub-object file. Pre-existing; unaffected by the dual-role classifier fix. +- **Required-member HasBeenSet handling (FIXED):** The C++ SDK tracks member presence via `HasBeenSet`, not required-ness. In C2J, `CppClientGenerator#generateSourceFiles` unconditionally clears `required` on EVERY modeled member ("so we can do a value has been set check on all fields"), then `addRequestIdToResults` injects `ResponseMetadata` as required — so the injected `ResponseMetadata` is the ONLY member rendered with no `HasBeenSet()` getter + flag `= true` (in a `useRequiredField=true` context: sub-object/request; a pure result uses `useRequiredField=false` so even it inits `= false`). Every modeled member — plain `@required` (JSON) AND `@required @clientOptional` (the 17 query/xml services) — renders a getter + `= false`. The Smithy plugin does NOT mirror C2J's model mutation (that would discard `@required`, which serde/validation will want). Instead `MemberRenderer` keys the always-present treatment on **recognizing the injected `ResponseMetadata`** (member named `ResponseMetadata` whose target is the `ResponseMetadata` structure — exactly how C2J identifies it), gated on `emitHasBeenSet` (the `useRequiredField` proxy: `forStructure`=true, `forResult`=false), excepting event-stream / raw-streaming-payload members. `@required` is left intact on all members. `GlobalTransforms.injectResponseMetadata` fails fast (`IllegalStateException`) if a model already defines a `ResponseMetadata` shape or member, so the name-based recognition stays unambiguous (verified: 0 of 433 models define one). `GlobalTransforms.RESPONSE_METADATA` is the single shared name constant. Verified end-to-end on EC2 (identical to C2J): 747 results init `= false`, 4 dual-role sub-objects (`Reservation`/`Snapshot`/`Volume`/`VolumeAttachment`) init `= true`; `VolumeDetail.Size` (`@required @clientOptional`) → getter + `= false`. NOTE: keying on `@clientOptional` would be WRONG — absent from the 380 JSON services whose plain `@required` members C2J also treats as optional (e.g. DynamoDB `GetItemRequest.TableName`). +- **Enum Windows-macro #undef guard (FIXED):** C2J's `ModelEnumHeader.vm` wraps enum values that collide with a Windows preprocessor macro in `#if defined(_WIN32) && defined(X) / #undef X / #endif`, driven by `PlatformAndKeywordSanitizer.PREDEFINED_SYMBOLS_MAPPING` (namespace-keyed: `EC2→interface`, `DynamoDB→IN`, `S3Crt→IGNORE`). The Smithy `EnumRenderer.renderHeader` now emits the same guard (before the namespace block) via `predefinedWindowsSymbols(serviceNamespace, values)`, mirroring that mapping. Fixes e.g. `NetworkInterfaceType.h` (`interface` value). Per-service/namespace keyed; other services with the same value do not emit it. +- **ShapeClassifier dual-role fix (FIXED):** structures that are both an operation output AND a member target (e.g. lambda FunctionConfiguration/AliasConfiguration/EventSourceMappingConfiguration/Concurrency/FunctionEventInvokeConfig) are now emitted as sub-objects too, matching C2J. Verified: lambda Only-in-C2J model files 10 -> 0; no spurious over-emission; sqs unchanged. +- **Deprecated-orphan dead files (global, ACCEPTED):** When a shape is reachable ONLY through `@deprecated` member(s), C2J still emits a model file for it, while the Smithy plugin omits it. Root cause is a C2J bug: `C2jModelToGeneratorModelTransformer.removeUnreferencedShapes()` is a single, non-transitive pass over `referencedBy`, so it removes only the first-order orphan (usually the intermediate list/map, which emits no file) and still emits the struct/enum that list pointed at as a dead, unreferenced file. The Smithy plugin's `computeReachableShapes` walks only surviving edges and correctly omits the whole orphan subtree. **Proven safe:** across all 433 services this drops files in 17 (e.g. ec2: AssociatedTargetNetwork/AssociatedNetworkType/ElasticGpuSpecification/ElasticInferenceAccelerator/LaunchTemplateElasticInferenceAccelerator; guardduty: 32) with **0 dangling references** — a shape shared with any non-deprecated reference is always kept (verified by `GlobalTransformsTest.dropDeprecatedMembers_sharedTargetSurvivesViaNonDeprecatedReference`). Smithy output is strictly cleaner; accepted rather than replicating C2J's dead files. + +## rds +- SourceRegion member injected by SourceRegionTransform is present and structurally identical to C2J at the member/accessor level (verified Task 2). Remaining rds diffs are the two global deltas above. + +## docdb +_(none yet)_ + +## neptune +_(none yet)_ + +## lambda +_(none yet)_ + +## sqs +_(none yet)_ + +## apigateway +_(none yet)_ + +## apigatewayv2 +_(none yet)_ + +## ec2 +- Result naming: operation-output result classes use `Response` via ResultRenderer+ShapeUtil.getResultSuffix; nested `*Result` domain structs renamed to `*Response` by Ec2Transforms. Verified 0 Result/Response file mismatches vs C2J. +- SpotInstanceState `disabled` value present (parity). +- SecureBlobAttributeValue (FIXED via Ec2Transforms): upstream `aws/aws-models` itself diverges — the C2J `ec2//service-2.json` models `ModifyInstanceAttributeRequest.UserData -> SecureBlobAttributeValue -> SecureBlob(@sensitive)`, but the upstream Smithy `ec2/smithy/model.json` still targets the non-sensitive `BlobAttributeValue` (verified against upstream on master). Re-syncing the Smithy model would NOT fix it (upstream Smithy lacks the shape). `Ec2Transforms.addSecureBlobUserData` mirrors the C2J modeling in the Smithy model at generation time: adds `SecureBlob`(@sensitive -> CryptoBuffer) + `SecureBlobAttributeValue{Value}` and repoints `UserData`, which orphans `BlobAttributeValue` so it drops from the emitted set exactly as in C2J. Self-retires (no-op) once the upstream Smithy model catches up. Temporary compensation for upstream data lag; the durable fix is an upstream aws-models correction. Note: the generated `SecureBlobAttributeValue.{h,cpp}` still differs from C2J only in the stubbed-serde bodies (global delta above). +- Deprecated-orphan dead files: 5 shapes / 10 files (AssociatedTargetNetwork, AssociatedNetworkType, ElasticGpuSpecification, ElasticInferenceAccelerator, LaunchTemplateElasticInferenceAccelerator) — see global "Deprecated-orphan dead files" delta above. +- OUT OF SCOPE (remain C2J, documented): ~180 legacy error-code injection, CopySnapshot presign, custom endpoint-enum template. These are client/error/endpoint artifacts, not model-shape. +- Remaining diffs are the two global deltas (stubbed serde, doc reflow). + +## S3 serde-phased customizations (deferred until Smithy serde lands) +- `markChecksumMembers` (S3 `CHECKSUM_MEMBERS_ENUMS`): checksum members drive request serialization + only; no model-header delta today. Implement as an `S3Transforms` marker step when serde is + un-stubbed. Map (member → algorithm value): ChecksumCRC32→CRC32, ChecksumCRC32C→CRC32C, + ChecksumSHA1→SHA1, ChecksumSHA256→SHA256, ChecksumSHA512→SHA512, ChecksumXXHASH64→XXHASH64, + ChecksumXXHASH3→XXHASH3, ChecksumXXHASH128→XXHASH128, ChecksumMD5→MD5. (ChecksumCRC64NVME NOT mapped.) +- `injectAccessLogTagQuery` (S3 `customizedAccessLogTag` querystring map on every request): serde + (`AddQueryStringParameters`) only; no model-header delta today. Implement with serde. + +### Task 7 investigation note (evidence correction — decision: DEFER both) +The two "serde only" labels above are imprecise: **both customizations DO produce an observable +model-HEADER delta today** in C2J vs the current `--use-smithy-models` output. They are still +deferred because **neither is closeable by a standalone `S3Transforms` marker/injection transform +without renderer or serde work** (the Task 3–6 pattern does not apply cleanly). Evidence: + +- **Checksum (header delta, needs MemberRenderer support):** `markChecksumMembers` sets + `ShapeMember.checksumMember/checksumEnumMember`, which C2J's *header* template + `ModelClassMembersAndInlines.vm` (lines 56–59, 100–101) consumes to emit a setter side-effect — + e.g. `SetChecksumCRC32(...)` also calls `SetChecksumAlgorithm(ChecksumAlgorithm::CRC32);` (and a + `const char*` overload). Confirmed present in C2J `generated/.../PutObjectRequest.h` + (`SetChecksumCRC32/CRC32C/SHA1/SHA256/SHA512/MD5/XXHASH64/XXHASH3/XXHASH128`), and correctly ABSENT + on the unmapped `ChecksumCRC64NVME`. The Smithy `MemberRenderer` setter bodies emit only + `HasBeenSet = true; ...` and never `SetChecksumAlgorithm(...)`; `RequestRenderer.renderChecksumImpls` + only handles the separate `@httpChecksum` trait impls (`GetChecksumAlgorithmName`, `ChecksumAlgorithmIsSet`, + etc. — the `ModelClassChecksumMembers.vm` concern), not the value-member setter side-effect. So a + *marker* transform alone is inert: closing this delta requires `MemberRenderer` to grow bespoke + logic that reads the marker. Deferred to the render/serde phase. + +- **Access-log tag (header delta, but query-binding/serde-entangled):** `injectAccessLogTagQuery` + (S3RestXmlCppClientGenerator.java ~299–340) injects a real `customizedAccessLogTag` + `map` member (`location=querystring`, `customizedQuery=true`) into EVERY request + input. C2J's header template renders full accessors — confirmed in C2J + `generated/.../PutObjectRequest.h` and others (`GetCustomizedAccessLogTag`, + `SetCustomizedAccessLogTag`, `WithCustomizedAccessLogTag`, `AddCustomizedAccessLogTag`, + `m_customizedAccessLogTag`, `m_customizedAccessLogTagHasBeenSet`). The Smithy S3 model + (`smithy/api-descriptions/s3.json`) has ZERO occurrences and no Smithy transform injects it, so the + member is absent from the Smithy header. An injection transform *could* render the accessors, but a + bare injection is incorrect: the member is a querystring/`customizedQuery` member whose only purpose + is serialization via `AddQueryStringParametersToRequest.vm` (the `$shape.customizedQuery` loop). + For restXml, an unbound injected member would be misclassified as a payload member; a correct + injection must carry the query-param binding (`@httpQueryParams`), which is a serde concern that is + currently stubbed. Deferred: implement alongside the querystring serde. From a28c2f3b4cf54e7ed946d657f3c9efc41bc6f393 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 13:23:23 -0400 Subject: [PATCH 24/44] Smithy: S3Transforms injects customizedAccessLogTag request member --- docs/superpowers/plans/parity-deltas.md | 49 ++++++++------ .../model/transforms/S3Transforms.java | 39 ++++++++++- .../model/transforms/S3TransformsTest.java | 67 +++++++++++++++++++ 3 files changed, 134 insertions(+), 21 deletions(-) diff --git a/docs/superpowers/plans/parity-deltas.md b/docs/superpowers/plans/parity-deltas.md index a7ba6d78859..f6aa6813a8a 100644 --- a/docs/superpowers/plans/parity-deltas.md +++ b/docs/superpowers/plans/parity-deltas.md @@ -50,14 +50,23 @@ _(none yet)_ un-stubbed. Map (member → algorithm value): ChecksumCRC32→CRC32, ChecksumCRC32C→CRC32C, ChecksumSHA1→SHA1, ChecksumSHA256→SHA256, ChecksumSHA512→SHA512, ChecksumXXHASH64→XXHASH64, ChecksumXXHASH3→XXHASH3, ChecksumXXHASH128→XXHASH128, ChecksumMD5→MD5. (ChecksumCRC64NVME NOT mapped.) -- `injectAccessLogTagQuery` (S3 `customizedAccessLogTag` querystring map on every request): serde - (`AddQueryStringParameters`) only; no model-header delta today. Implement with serde. - -### Task 7 investigation note (evidence correction — decision: DEFER both) +- `injectAccessLogTagQuery` (S3 `customizedAccessLogTag` querystring map on every request): + **IMPLEMENTED** (Task 7) — `S3Transforms.injectAccessLogTagQuery` injects a `customizedAccessLogTag` + `map` member (targeting `com.amazonaws.s3#CustomizedAccessLogTag`, key+value + `smithy.api#String`) onto every operation request shape, appended last, idempotent. This closes the + `.h` member-accessor delta (`GetCustomizedAccessLogTag` / `SetCustomizedAccessLogTag` / + `WithCustomizedAccessLogTag` / `AddCustomizedAccessLogTag` / `m_customizedAccessLogTag`). The + querystring binding (`location=querystring`, `customizedQuery=true` → `AddQueryStringParameters` + serde) is still DEFERRED until Smithy serde lands; no `@httpQuery`/`@httpQueryParams` trait is + attached yet, to avoid perturbing stubbed request emission. + +### Task 7 investigation note (evidence correction — access-log IMPLEMENTED, checksum DEFERRED) The two "serde only" labels above are imprecise: **both customizations DO produce an observable -model-HEADER delta today** in C2J vs the current `--use-smithy-models` output. They are still -deferred because **neither is closeable by a standalone `S3Transforms` marker/injection transform -without renderer or serde work** (the Task 3–6 pattern does not apply cleanly). Evidence: +model-HEADER delta today** in C2J vs the current `--use-smithy-models` output. Per a later controller +decision, the **access-log tag member injection is now IMPLEMENTED** (the `.h` accessors are a pure +model-shape delta, closeable by a standalone `S3Transforms` injection; only its querystring serde +binding is deferred). **Checksum stays DEFERRED** — it is not closeable by a marker transform without +renderer work. Evidence: - **Checksum (header delta, needs MemberRenderer support):** `markChecksumMembers` sets `ShapeMember.checksumMember/checksumEnumMember`, which C2J's *header* template @@ -72,17 +81,19 @@ without renderer or serde work** (the Task 3–6 pattern does not apply cleanly) *marker* transform alone is inert: closing this delta requires `MemberRenderer` to grow bespoke logic that reads the marker. Deferred to the render/serde phase. -- **Access-log tag (header delta, but query-binding/serde-entangled):** `injectAccessLogTagQuery` - (S3RestXmlCppClientGenerator.java ~299–340) injects a real `customizedAccessLogTag` - `map` member (`location=querystring`, `customizedQuery=true`) into EVERY request - input. C2J's header template renders full accessors — confirmed in C2J - `generated/.../PutObjectRequest.h` and others (`GetCustomizedAccessLogTag`, +- **Access-log tag (header delta — IMPLEMENTED; query-binding serde deferred):** + `injectAccessLogTagQuery` (S3RestXmlCppClientGenerator.java ~299–340) injects a real + `customizedAccessLogTag` `map` member (`location=querystring`, + `customizedQuery=true`) into EVERY request input. C2J's header template renders full accessors — + confirmed in C2J `generated/.../PutObjectRequest.h` and others (`GetCustomizedAccessLogTag`, `SetCustomizedAccessLogTag`, `WithCustomizedAccessLogTag`, `AddCustomizedAccessLogTag`, `m_customizedAccessLogTag`, `m_customizedAccessLogTagHasBeenSet`). The Smithy S3 model - (`smithy/api-descriptions/s3.json`) has ZERO occurrences and no Smithy transform injects it, so the - member is absent from the Smithy header. An injection transform *could* render the accessors, but a - bare injection is incorrect: the member is a querystring/`customizedQuery` member whose only purpose - is serialization via `AddQueryStringParametersToRequest.vm` (the `$shape.customizedQuery` loop). - For restXml, an unbound injected member would be misclassified as a payload member; a correct - injection must carry the query-param binding (`@httpQueryParams`), which is a serde concern that is - currently stubbed. Deferred: implement alongside the querystring serde. + (`smithy/api-descriptions/s3.json`) has ZERO occurrences. `S3Transforms.injectAccessLogTagQuery` + now mirrors the C2J injection at the model-shape level: it appends the `customizedAccessLogTag` + `map` member to every request input (idempotent), closing the `.h` accessor delta. + The query-string binding is intentionally NOT modeled yet: no `@httpQuery`/`@httpQueryParams` trait + is attached, because that would engage the (stubbed) serde/render path and risk perturbing request + emission. For restXml an unbound member would be misclassified as a payload member during serde; + the correct query-param binding (`customizedQuery` loop in `AddQueryStringParametersToRequest.vm`) + lands with the querystring serde work. Byte-parity of the querystring serialization is a Task 9 + follow-up once Smithy serde is un-stubbed. diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index babe2985262..e369742e0fc 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -10,6 +10,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -51,8 +52,42 @@ private static Model apply(Model model, ServiceShape service) { if (!"s3".equals(name) && !"s3-crt".equals(name)) { return model; } - return normalizeReplicationStatus(expandBucketLocationConstraint( - hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service)))); + return injectAccessLogTagQuery(normalizeReplicationStatus(expandBucketLocationConstraint( + hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service)))), service); + } + + // C2J's S3RestXmlCppClientGenerator appends a `customizedAccessLogTag` map member + // to every operation request shape. It renders as an ordinary map member in the .h; the query- + // string binding (location=querystring, customizedQuery=true) is a serde concern deferred until + // Smithy serde lands, so no @httpQuery/@httpQueryParams trait is attached here. + private static Model injectAccessLogTagQuery(Model model, ServiceShape service) { + ShapeId mapId = ShapeId.fromParts("com.amazonaws.s3", "CustomizedAccessLogTag"); + ShapeId stringId = ShapeId.from("smithy.api#String"); + + Set inputShapes = TopDownIndex.of(model).getContainedOperations(service).stream() + .map(OperationShape::getInputShape) + .collect(Collectors.toSet()); + List updated = model.shapes(StructureShape.class) + .filter(s -> inputShapes.contains(s.getId())) + .filter(s -> s.getMember("customizedAccessLogTag").isEmpty()) + .toList(); + if (updated.isEmpty()) { + return model; // no request shape needs the member (idempotent / no operations). + } + + List replacements = new ArrayList<>(); + if (model.getShape(mapId).isEmpty()) { + replacements.add(MapShape.builder().id(mapId).key(stringId).value(stringId).build()); + } + for (StructureShape req : updated) { + replacements.add(req.toBuilder() + .addMember(MemberShape.builder() + .id(req.getId().withMember("customizedAccessLogTag")) + .target(mapId) + .build()) + .build()); + } + return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } // C2J collapses the model's split COMPLETE/COMPLETED ReplicationStatus values into a single diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 677d6181b13..399e4c3560d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -241,4 +241,71 @@ void injectsGetObjectId2AndRequestId() { assertEquals("x-amz-request-id", reqId.expectTrait(software.amazon.smithy.model.traits.HttpHeaderTrait.class).getValue()); } + + /** + * Builds an S3 model with a single operation whose input carries two ordinary members, so the + * appended-last ordering of the injected access-log tag member can be asserted. + */ + private static Model accessLogModel() { + StructureShape input = StructureShape.builder().id(NS + "#PutObjectRequest") + .addMember("Bucket", ShapeId.from("smithy.api#String")) + .addMember("Key", ShapeId.from("smithy.api#String")) + .build(); + StructureShape output = StructureShape.builder().id(NS + "#PutObjectOutput") + .addMember("ETag", ShapeId.from("smithy.api#String")) + .build(); + OperationShape op = OperationShape.builder().id(NS + "#PutObject") + .input(input.getId()).output(output.getId()).build(); + ServiceShape svc = ServiceShape.builder().id(NS + "#AmazonS3").version("2006-03-01") + .addTrait(ServiceTrait.builder().sdkId("S3").arnNamespace("s3") + .cloudFormationName("S3").cloudTrailEventSource("s3.amazonaws.com").build()) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(input, output, op, svc).assemble().unwrap(); + } + + private static ServiceShape s3ServiceOf(Model m) { + return m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); + } + + @Test + void injectsCustomizedAccessLogTagIntoRequestAsStringMapAppendedLast() { + Model m = accessLogModel(); + Model out = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); + + StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); + MemberShape tag = input.getMember("customizedAccessLogTag").orElseThrow(); + + // Target must be a Map (both key and value render as Aws::String). + software.amazon.smithy.model.shapes.MapShape mapShape = out.expectShape( + tag.getTarget(), software.amazon.smithy.model.shapes.MapShape.class); + assertEquals("smithy.api#String", mapShape.getKey().getTarget().toString()); + assertEquals("smithy.api#String", mapShape.getValue().getTarget().toString()); + + // Appended after all existing members, preserving prior order. + java.util.List order = new java.util.ArrayList<>(input.getAllMembers().keySet()); + assertEquals(java.util.List.of("Bucket", "Key", "customizedAccessLogTag"), order, + "access-log tag member appended last"); + } + + @Test + void doesNotInjectCustomizedAccessLogTagIntoOutput() { + Model m = accessLogModel(); + Model out = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); + + StructureShape output = out.expectShape(ShapeId.from(NS + "#PutObjectOutput"), StructureShape.class); + assertFalse(output.getMember("customizedAccessLogTag").isPresent(), + "output shape must not gain the access-log tag member"); + } + + @Test + void accessLogTagInjectionIsIdempotent() { + Model m = accessLogModel(); + Model once = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); + Model twice = S3Transforms.asTransform().apply(once, s3ServiceOf(once)); + + StructureShape input = twice.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); + long count = input.getAllMembers().keySet().stream() + .filter("customizedAccessLogTag"::equals).count(); + assertEquals(1, count, "re-applying must not duplicate the injected member"); + } } From e198be955012836bfcb3caaec0de609859b84a81 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 13:35:13 -0400 Subject: [PATCH 25/44] Smithy: S3ControlTransforms adds top-level HostId to result classes --- .../generators/model/MemberRenderer.java | 26 ++++++++ .../generators/model/ModelCodegenPlugin.java | 5 +- .../model/renderers/ResultRenderer.java | 30 +++++++++ .../model/transforms/S3ControlTransforms.java | 55 +++++++++++++++++ .../model/transforms/TopLevelHostIdTrait.java | 25 ++++++++ .../generators/model/ResultRendererTest.java | 61 +++++++++++++++++++ .../transforms/S3ControlTransformsTest.java | 48 +++++++++++++++ 7 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java index 6894d604f18..fb130f6ae65 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java @@ -295,6 +295,32 @@ public static void renderRequestIdAccessors(CppWriter writer, String className, writer.write("///@}"); } + /** + * Renders the top-level {@code HostId} (x-amz-id-2) accessor group emitted by S3 Control result + * headers immediately after the {@code RequestId} group: {@code GetHostId} / templated + * {@code SetHostId} / templated {@code WithHostId}. Callers gate emission on + * {@code TopLevelHostIdTrait} and separately emit the {@code m_hostId} field and its + * {@code HasBeenSet} flag in the private section. Matches C2J's {@code addToAllResultsShape} + * HostId member, including the doc string. + */ + public static void renderHostIdAccessors(CppWriter writer, String className) { + writer.write(""); + writer.write("///@{"); + writeDocComment(writer, "x-amz-id-2 header value, also known as Host Id"); + writer.write("inline const Aws::String& GetHostId() const { return m_hostId; }"); + writer.write("template "); + writer.openBlock("void SetHostId(HostIdT&& value) {", "}", () -> { + writer.write("m_hostIdHasBeenSet = true;"); + writer.write("m_hostId = std::forward(value);"); + }); + writer.write("template "); + writer.openBlock("$L& WithHostId(HostIdT&& value) {", "}", className, () -> { + writer.write("SetHostId(std::forward(value));"); + writer.write("return *this;"); + }); + writer.write("///@}"); + } + /** * Writes a single private data member declaration. {@code @idempotencyToken} members are * brace-initialized with {@code Aws::Utils::UUID::PseudoRandomUUID()} so a caller who omits diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index b38e4425aa6..5fe596ffbec 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -13,6 +13,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.Ec2Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LambdaTransforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.S3ControlTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.S3Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SourceRegionTransform; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SqsTransforms; @@ -60,8 +61,8 @@ public void execute(PluginContext context) { Ec2Transforms.asTransform(), AccessAnalyzerTransforms.asTransform(), DynamoDbTransforms.asTransform(), - S3Transforms.asTransform() - // Future: S3ControlTransforms.asTransform(), etc. + S3Transforms.asTransform(), + S3ControlTransforms.asTransform() )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java index 438c2244e3e..af8b44a3006 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java @@ -13,6 +13,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.ResultInfo; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.TopLevelHostIdTrait; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.StructureShape; @@ -114,6 +115,13 @@ private void renderHeader(CppWriterDelegator writerDelegator, MemberRenderer.renderRequestIdAccessors(writer, className); } + // The top-level HostId (x-amz-id-2) group is per-service (S3 Control only), driven + // by the internal marker rather than a protocol flag, and always follows RequestId. + boolean topLevelHostId = shape.hasTrait(TopLevelHostIdTrait.class); + if (topLevelHostId) { + MemberRenderer.renderHostIdAccessors(writer, className); + } + writer.write("inline Aws::Http::HttpResponseCode GetHttpResponseCode() const { return m_HttpResponseCode; }"); writer.write(""); @@ -127,11 +135,19 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.write(""); writer.write("Aws::String m_requestId;"); } + if (topLevelHostId) { + // C2J declares m_hostId in its own group right after m_requestId. + writer.write(""); + writer.write("Aws::String m_hostId;"); + } writer.write("Aws::Http::HttpResponseCode m_HttpResponseCode;"); members.renderHasBeenSetFlags(writer); if (topLevelRequestId) { writer.write("bool m_requestIdHasBeenSet = false;"); } + if (topLevelHostId) { + writer.write("bool m_hostIdHasBeenSet = false;"); + } }); writer.write(""); }); @@ -232,6 +248,13 @@ private void renderStreamingHeader(CppWriterDelegator writerDelegator, MemberRenderer.renderRequestIdAccessors(writer, className); + // S3 Control has no streaming results today; the marker is only stamped on its + // outputs, so this block is a defensive no-op for every current streaming result. + boolean topLevelHostId = shape.hasTrait(TopLevelHostIdTrait.class); + if (topLevelHostId) { + MemberRenderer.renderHostIdAccessors(writer, className); + } + writer.write("inline Aws::Http::HttpResponseCode GetHttpResponseCode() const { return m_HttpResponseCode; }"); writer.write(""); @@ -242,10 +265,17 @@ private void renderStreamingHeader(CppWriterDelegator writerDelegator, members.renderDataMembers(writer); writer.write(""); writer.write("Aws::String m_requestId;"); + if (topLevelHostId) { + writer.write(""); + writer.write("Aws::String m_hostId;"); + } writer.write("Aws::Http::HttpResponseCode m_HttpResponseCode;"); writer.write("bool $LHasBeenSet = false;", streamField); members.renderHasBeenSetFlags(writer); writer.write("bool m_requestIdHasBeenSet = false;"); + if (topLevelHostId) { + writer.write("bool m_hostIdHasBeenSet = false;"); + } }); writer.write(""); }); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java new file mode 100644 index 00000000000..38c61eba96f --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java @@ -0,0 +1,55 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; + +import java.util.ArrayList; +import java.util.List; + +/** + * S3 Control model parity with the legacy C2J {@code S3ControlRestXmlCppClientGenerator}, whose + * {@code addRequestIdToResults} adds BOTH a top-level {@code RequestId} and a top-level + * {@code HostId} (x-amz-id-2) to every result. RequestId is already emitted generically + * ({@code ProtocolTraits.resultHasTopLevelRequestId()}); this transform closes the HostId gap by + * marking each operation-output structure with {@link TopLevelHostIdTrait}, which + * {@code ResultRenderer} turns into the top-level HostId accessor group. Self-guards on the raw + * smithy service name {@code s3-control} ({@code ServiceNameUtil.getSmithyServiceName} lowercases + * the {@code S3 Control} sdkId and replaces the space with a hyphen; the {@code s3-control -> + * s3control} c2jMap remap is applied later by the plugin, not here). + */ +public final class S3ControlTransforms { + + private S3ControlTransforms() {} + + public static ModelTransform asTransform() { + return S3ControlTransforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"s3-control".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + TopDownIndex index = TopDownIndex.of(model); + List marked = new ArrayList<>(); + for (OperationShape op : index.getContainedOperations(service)) { + model.getShape(op.getOutputShape()).flatMap(Shape::asStructureShape).ifPresent(out -> { + if (!out.hasTrait(TopLevelHostIdTrait.class)) { + marked.add(out.toBuilder().addTrait(new TopLevelHostIdTrait()).build()); + } + }); + } + if (marked.isEmpty()) { + return model; + } + return model.toBuilder().addShapes(marked.toArray(new Shape[0])).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java new file mode 100644 index 00000000000..70f0eab371c --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java @@ -0,0 +1,25 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link S3ControlTransforms} on each + * S3 Control operation-output structure. {@code ResultRenderer} emits the top-level {@code HostId} + * (x-amz-id-2) accessor group for marker-bearing result shapes — mirroring how the sibling + * top-level {@code RequestId} is emitted — so S3 Control results match C2J + * ({@code addToAllResultsShape("hostId", ...)}). Kept as a marker + generic renderer rule (not a + * service-name {@code if}) so the renderer stays service-agnostic. + */ +public final class TopLevelHostIdTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#topLevelHostId"); + + public TopLevelHostIdTrait() { + super(ID, Node.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java index cbbe716190b..7f01064501c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java @@ -304,4 +304,65 @@ void statusCodeMember_setFromResponseCode() { String cpp = renderResultSource(statusCodeResultModel(), "StatResult.cpp"); assertTrue(cpp.contains("m_status = static_cast(result.GetResponseCode());"), cpp); } + + /** + * A one-member rest-xml output operation whose output structure optionally carries the internal + * {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.TopLevelHostIdTrait} + * marker (as {@code S3ControlTransforms} stamps it). + */ + private static Model hostIdResultModel(boolean marked) { + StringShape str = StringShape.builder().id("com.example#Str").build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoThingInput").addMember("name", str.getId()).build(); + StructureShape.Builder outputBuilder = StructureShape.builder() + .id("com.example#DoThingOutput").addMember("field", str.getId()); + if (marked) { + outputBuilder.addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators + .model.transforms.TopLevelHostIdTrait()); + } + StructureShape output = outputBuilder.build(); + OperationShape op = OperationShape.builder() + .id("com.example#DoThing").input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(software.amazon.smithy.aws.traits.protocols.RestXmlTrait.builder().build()) + .addOperation(op.getId()).build(); + return Model.builder().addShapes(str, input, output, op, service).build(); + } + + @Test + void hostIdTrait_rendersHostIdGroupAfterRequestId() { + String h = renderResultSource(hostIdResultModel(true), "DoThingResult.h"); + assertTrue(h.contains("inline const Aws::String& GetHostId() const { return m_hostId; }"), h); + assertTrue(h.contains("x-amz-id-2 header value, also known as Host Id"), h); + assertTrue(h.contains("Aws::String m_hostId;"), h); + assertTrue(h.contains("bool m_hostIdHasBeenSet = false;"), h); + + // The HostId accessor group renders immediately after the RequestId group. + int reqAccessor = h.indexOf("GetRequestId"); + int hostAccessor = h.indexOf("GetHostId"); + assertTrue(reqAccessor >= 0 && hostAccessor > reqAccessor, + "HostId accessor group must follow the RequestId group: " + h); + + // m_hostId is declared immediately after m_requestId in the private section. + int reqMember = h.indexOf("Aws::String m_requestId;"); + int hostMember = h.indexOf("Aws::String m_hostId;"); + assertTrue(reqMember >= 0 && hostMember > reqMember, + "m_hostId must be declared after m_requestId: " + h); + + int reqFlag = h.indexOf("bool m_requestIdHasBeenSet = false;"); + int hostFlag = h.indexOf("bool m_hostIdHasBeenSet = false;"); + assertTrue(reqFlag >= 0 && hostFlag > reqFlag, + "m_hostIdHasBeenSet must be declared after m_requestIdHasBeenSet: " + h); + } + + @Test + void noHostIdTrait_omitsHostIdGroup() { + String h = renderResultSource(hostIdResultModel(false), "DoThingResult.h"); + // A rest-xml result still gets the sibling top-level RequestId group ... + assertTrue(h.contains("GetRequestId"), h); + // ... but no HostId group without the marker trait. + assertFalse(h.contains("GetHostId"), h); + assertFalse(h.contains("m_hostId"), h); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java new file mode 100644 index 00000000000..b8b2e9f35d1 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java @@ -0,0 +1,48 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; + +import static org.junit.jupiter.api.Assertions.*; + +class S3ControlTransformsTest { + static final String NS = "com.amazonaws.s3control"; + + static Model model(String sdkId) { + StructureShape result = StructureShape.builder().id(NS + "#CreateAccessPointResult") + .addMember("AccessPointArn", ShapeId.from("smithy.api#String")).build(); + StructureShape req = StructureShape.builder().id(NS + "#CreateAccessPointRequest").build(); + OperationShape op = OperationShape.builder().id(NS + "#CreateAccessPoint") + .input(req.getId()).output(result.getId()).build(); + ServiceShape svc = ServiceShape.builder().id(NS + "#AWSS3Control").version("2018-08-20") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("s3") + .cloudFormationName("S3Control").cloudTrailEventSource("s3control.amazonaws.com").build()) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(req, result, op, svc).assemble().unwrap(); + } + + static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from(NS + "#AWSS3Control"), ServiceShape.class); + } + + @Test + void marksResultShapesWithHostIdTrait() { + Model m = model("S3 Control"); + Model out = S3ControlTransforms.asTransform().apply(m, service(m)); + assertTrue(out.expectShape(ShapeId.from(NS + "#CreateAccessPointResult")) + .hasTrait(TopLevelHostIdTrait.class), "result shape marked"); + } + + @Test + void noOpForOtherService() { + Model m = model("SomethingElse"); + Model out = S3ControlTransforms.asTransform().apply(m, service(m)); + assertSame(m, out); + } +} From b3ddae719a054be3e737fefe0a3e4b615202a592 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 15:58:53 -0400 Subject: [PATCH 26/44] =?UTF-8?q?Smithy:=20fix=20S3=20byte-parity=20?= =?UTF-8?q?=E2=80=94=20CopyObjectResult=20member=20rename,=20GetObject=20I?= =?UTF-8?q?d2-only,=20region=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/plans/parity-deltas.md | 11 +++++ .../model/transforms/S3Transforms.java | 37 ++++++++++---- .../model/transforms/S3TransformsTest.java | 48 +++++++++++++++---- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/docs/superpowers/plans/parity-deltas.md b/docs/superpowers/plans/parity-deltas.md index f6aa6813a8a..0aa51d260e8 100644 --- a/docs/superpowers/plans/parity-deltas.md +++ b/docs/superpowers/plans/parity-deltas.md @@ -44,6 +44,17 @@ _(none yet)_ - OUT OF SCOPE (remain C2J, documented): ~180 legacy error-code injection, CopySnapshot presign, custom endpoint-enum template. These are client/error/endpoint artifacts, not model-shape. - Remaining diffs are the two global deltas (stubbed serde, doc reflow). +## S3 accepted divergences +- **Expires deprecation note on both GetObjectResult AND HeadObjectResult:** The Smithy path applies + the `Expires` "Deprecated: Please use ExpiresString instead." doc-comment note to every operation + output that carries `Expires`, so it appears on both `GetObjectResult` and `HeadObjectResult`. C2J + emits the note only on `GetObjectResult` — an artifact of C2J deduping the customization across a + `Set`, which collapses the shared `Expires` member so the note lands on just one + result. The Smithy behavior is intentional and more consistent (both results describe the same + deprecated field identically). This is doc-comment-only and non-structural — accessors, member + order, and wire behavior are unchanged — so it is accepted rather than replicating C2J's + dedup-driven omission. No transform change (`addExpiresCustomization` is unchanged). + ## S3 serde-phased customizations (deferred until Smithy serde lands) - `markChecksumMembers` (S3 `CHECKSUM_MEMBERS_ENUMS`): checksum members drive request serialization only; no model-header delta today. Implement as an `S3Transforms` marker step when serde is diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index e369742e0fc..23e829db2f2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -7,6 +7,8 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; import com.amazonaws.util.awsclientsmithygenerator.generators.model.EnumRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.EnumShape; @@ -53,7 +55,7 @@ private static Model apply(Model model, ServiceShape service) { return model; } return injectAccessLogTagQuery(normalizeReplicationStatus(expandBucketLocationConstraint( - hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model), service)))), service); + hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model, service), service)))), service); } // C2J's S3RestXmlCppClientGenerator appends a `customizedAccessLogTag` map member @@ -124,7 +126,7 @@ private static Model normalizeReplicationStatus(Model model) { // Confirmed delta at implementation time against the live s3.json BucketLocationConstraint enum; // the other 18 C2J regions are already present upstream. - private static final List MISSING_REGIONS = List.of("us-east-1", "us-iso-west-1"); + private static final List MISSING_REGIONS = List.of("us-iso-west-1", "us-east-1"); private static Model expandBucketLocationConstraint(Model model) { Optional enumShape = model.shapes() @@ -147,6 +149,10 @@ private static Map regionNameValueMap() { return map; } + // C2J's GetObjectResult carries an x-amz-id-2 header member (Id2) plus the standard RequestId. + // The RequestId is supplied by ResultRenderer's top-level RequestId group for rest-xml results + // (resultHasTopLevelRequestId() == true), which byte-matches C2J; injecting a modeled RequestId + // member here would duplicate it. So inject only Id2. private static Model hackGetObjectResult(Model model) { String ns = "com.amazonaws.s3"; ShapeId outputId = ShapeId.fromParts(ns, "GetObjectOutput"); @@ -155,13 +161,11 @@ private static Model hackGetObjectResult(Model model) { return model; // no GetObjectOutput: nothing to do. } StructureShape output = outputOpt.get(); - if (output.getMember("Id2").isPresent() && output.getMember("RequestId").isPresent()) { - return model; // already injected (idempotent) — or upstream added them. + if (output.getMember("Id2").isPresent()) { + return model; // already injected (idempotent) — or upstream added it. } ShapeId id2ShapeId = ShapeId.fromParts(ns, "ObjectId2"); - ShapeId reqIdShapeId = ShapeId.fromParts(ns, "ObjectRequestId"); StringShape id2Shape = StringShape.builder().id(id2ShapeId).build(); - StringShape reqIdShape = StringShape.builder().id(reqIdShapeId).build(); StructureShape.Builder b = StructureShape.builder().id(output.getId()); output.getAllTraits().values().forEach(b::addTrait); @@ -169,12 +173,15 @@ private static Model hackGetObjectResult(Model model) { b.addMember(m.getMemberName(), m.getTarget(), mb -> m.getAllTraits().values().forEach(mb::addTrait))); b.addMember("Id2", id2ShapeId, mb -> mb.addTrait(new HttpHeaderTrait("x-amz-id-2"))); - b.addMember("RequestId", reqIdShapeId, mb -> mb.addTrait(new HttpHeaderTrait("x-amz-request-id"))); - return model.toBuilder().addShapes(id2Shape, reqIdShape, b.build()).build(); + return model.toBuilder().addShapes(id2Shape, b.build()).build(); } - private static Model renameCopyObjectResult(Model model) { + // C2J renames both the CopyObjectResult domain shape (to CopyObjectResultDetails) and the + // CopyObjectOutput member that references it, so the member renders as GetCopyObjectResultDetails + // while keeping its CopyObjectResult wire name. renameMember pins @xmlName("CopyObjectResult") + // for rest-xml so the wire key survives the member-name change. + private static Model renameCopyObjectResult(Model model, ServiceShape service) { String ns = "com.amazonaws.s3"; ShapeId oldId = ShapeId.fromParts(ns, "CopyObjectResult"); ShapeId newId = ShapeId.fromParts(ns, "CopyObjectResultDetails"); @@ -185,7 +192,17 @@ private static Model renameCopyObjectResult(Model model) { throw new IllegalStateException("S3 collision: '" + newId + "' already exists; cannot " + "rename '" + oldId + "' onto it."); } - return ModelTransformer.create().renameShapes(model, Map.of(oldId, newId)); + Model renamed = ModelTransformer.create().renameShapes(model, Map.of(oldId, newId)); + + ShapeId outputId = ShapeId.fromParts(ns, "CopyObjectOutput"); + Optional output = renamed.getShape(outputId).flatMap(Shape::asStructureShape); + if (output.isEmpty()) { + return renamed; // no CopyObjectOutput: shape rename suffices. + } + Protocol protocol = ProtocolResolver.resolve(service, renamed); + Optional updated = TransformSupport.renameMember( + output.get(), "CopyObjectResult", "CopyObjectResultDetails", protocol); + return updated.map(s -> renamed.toBuilder().addShape(s).build()).orElse(renamed); } private static final String EXPIRES_DEPRECATION = diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 399e4c3560d..ecaeda4b691 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -57,17 +57,38 @@ void noOpForS3WhenNothingToDo() { assertTrue(out.getShape(ShapeId.from(NS + "#AmazonS3")).isPresent()); } + static ServiceShape s3RestXmlService(String sdkId) { + return ServiceShape.builder().id(NS + "#AmazonS3").version("2006-03-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("s3") + .cloudFormationName("S3").cloudTrailEventSource("s3.amazonaws.com").build()) + .addTrait(software.amazon.smithy.aws.traits.protocols.RestXmlTrait.builder().build()) + .build(); + } + @Test - void renamesCopyObjectResultToDetails() { - ServiceShape svc = s3Service("S3"); + void renamesCopyObjectResultShapeAndMember() { + ServiceShape svc = s3RestXmlService("S3"); StructureShape copyResult = StructureShape.builder().id(NS + "#CopyObjectResult") .addMember("ETag", ShapeId.from("smithy.api#String")).build(); - Model m = modelWith(svc, copyResult); + StructureShape copyOutput = StructureShape.builder().id(NS + "#CopyObjectOutput") + .addMember("CopyObjectResult", copyResult.getId()).build(); + Model m = modelWith(svc, copyResult, copyOutput); Model out = S3Transforms.asTransform().apply(m, svc); + assertTrue(out.getShape(ShapeId.from(NS + "#CopyObjectResultDetails")).isPresent(), - "renamed to CopyObjectResultDetails"); + "shape renamed to CopyObjectResultDetails"); assertFalse(out.getShape(ShapeId.from(NS + "#CopyObjectResult")).isPresent(), - "old name gone"); + "old shape name gone"); + + StructureShape output = out.expectShape(ShapeId.from(NS + "#CopyObjectOutput"), StructureShape.class); + assertFalse(output.getMember("CopyObjectResult").isPresent(), + "old member name gone"); + MemberShape renamed = output.getMember("CopyObjectResultDetails").orElseThrow(); + assertEquals(NS + "#CopyObjectResultDetails", renamed.getTarget().toString(), + "renamed member still targets the renamed shape"); + assertEquals("CopyObjectResult", + renamed.expectTrait(software.amazon.smithy.model.traits.XmlNameTrait.class).getValue(), + "renamed member pins its original CopyObjectResult wire name via @xmlName"); } @Test @@ -181,6 +202,10 @@ void appendsMissingBucketLocationConstraintRegions() { // Member names are identifier-safe, matching the existing model form (hyphens -> underscores). assertTrue(result.getAllMembers().containsKey("us_east_1"), "identifier-safe member name"); assertTrue(result.getAllMembers().containsKey("us_iso_west_1"), "identifier-safe member name"); + // C2J appends the two regions in the order us_iso_west_1 then us_east_1. + assertEquals(java.util.List.of("us_west_2", "us_iso_west_1", "us_east_1"), + new java.util.ArrayList<>(result.getAllMembers().keySet()), + "appended regions follow C2J order: us_iso_west_1 before us_east_1"); } @Test @@ -223,7 +248,7 @@ void normalizesReplicationStatusCompleteToCompleted() { } @Test - void injectsGetObjectId2AndRequestId() { + void injectsGetObjectId2Only() { ServiceShape svc = s3Service("S3"); StructureShape getObjectOutput = StructureShape.builder().id(NS + "#GetObjectOutput") .addMember("ETag", ShapeId.from("smithy.api#String")).build(); @@ -231,15 +256,18 @@ void injectsGetObjectId2AndRequestId() { Model out = S3Transforms.asTransform().apply(m, svc); assertTrue(out.getShape(ShapeId.from(NS + "#ObjectId2")).isPresent()); - assertTrue(out.getShape(ShapeId.from(NS + "#ObjectRequestId")).isPresent()); StructureShape outShape = out.expectShape(ShapeId.from(NS + "#GetObjectOutput"), StructureShape.class); MemberShape id2 = outShape.getMember("Id2").orElseThrow(); assertEquals(NS + "#ObjectId2", id2.getTarget().toString()); assertEquals("x-amz-id-2", id2.expectTrait(software.amazon.smithy.model.traits.HttpHeaderTrait.class).getValue()); - MemberShape reqId = outShape.getMember("RequestId").orElseThrow(); - assertEquals("x-amz-request-id", - reqId.expectTrait(software.amazon.smithy.model.traits.HttpHeaderTrait.class).getValue()); + + // ResultRenderer supplies the top-level RequestId for rest-xml results; a modeled RequestId + // member would duplicate it and fail to compile, so it must not be injected here. + assertFalse(outShape.getMember("RequestId").isPresent(), + "no modeled RequestId member; the renderer emits RequestId"); + assertFalse(out.getShape(ShapeId.from(NS + "#ObjectRequestId")).isPresent(), + "ObjectRequestId shape must not be created"); } /** From 673e57121aafa225c883359b13611d98e83f8071 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Fri, 28 Aug 2026 16:05:36 -0400 Subject: [PATCH 27/44] Smithy: S3Transforms invert ExpiresString guard and add Unit-input guard --- .../model/transforms/S3Transforms.java | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 23e829db2f2..1c3aa9c7993 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -24,6 +24,7 @@ import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.HttpHeaderTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.transform.ModelTransformer; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -68,6 +69,9 @@ private static Model injectAccessLogTagQuery(Model model, ServiceShape service) Set inputShapes = TopDownIndex.of(model).getContainedOperations(service).stream() .map(OperationShape::getInputShape) + // smithy.api#Unit is a shared prelude StructureShape; mutating it would corrupt every + // Unit-input operation across the model, so never treat it as a request shape. + .filter(id -> !id.equals(UnitTypeTrait.UNIT)) .collect(Collectors.toSet()); List updated = model.shapes(StructureShape.class) .filter(s -> inputShapes.contains(s.getId())) @@ -231,34 +235,34 @@ private static Model addExpiresCustomization(Model model, ServiceShape service) replacements.add(StringShape.builder().id(expiresStringId).build()); } for (StructureShape struct : withExpires) { - if (struct.getMember("ExpiresString").isPresent()) { - continue; // already customized (idempotent). - } - MemberShape expires = struct.getAllMembers().get("Expires"); - StructureShape.Builder b = StructureShape.builder().id(struct.getId()); - struct.getAllTraits().values().forEach(b::addTrait); - for (MemberShape m : struct.getAllMembers().values()) { - if (m.getMemberName().equals("Expires")) { - // Rewrite Expires' documentation to prepend the deprecation note. - String existingDoc = m.getTrait(DocumentationTrait.class) - .map(DocumentationTrait::getValue).orElse(""); - b.addMember("Expires", m.getTarget(), mb -> { - m.getAllTraits().values().forEach(mb::addTrait); - if (!existingDoc.toLowerCase().contains("deprecated")) { - mb.addTrait(new DocumentationTrait(EXPIRES_DEPRECATION + existingDoc)); - } - }); - } else { - b.addMember(m.getMemberName(), m.getTarget(), - mb -> m.getAllTraits().values().forEach(mb::addTrait)); + // Only customize structs that lack ExpiresString (idempotent). + if (struct.getMember("ExpiresString").isEmpty()) { + MemberShape expires = struct.getAllMembers().get("Expires"); + StructureShape.Builder b = StructureShape.builder().id(struct.getId()); + struct.getAllTraits().values().forEach(b::addTrait); + for (MemberShape m : struct.getAllMembers().values()) { + if (m.getMemberName().equals("Expires")) { + // Rewrite Expires' documentation to prepend the deprecation note. + String existingDoc = m.getTrait(DocumentationTrait.class) + .map(DocumentationTrait::getValue).orElse(""); + b.addMember("Expires", m.getTarget(), mb -> { + m.getAllTraits().values().forEach(mb::addTrait); + if (!existingDoc.toLowerCase().contains("deprecated")) { + mb.addTrait(new DocumentationTrait(EXPIRES_DEPRECATION + existingDoc)); + } + }); + } else { + b.addMember(m.getMemberName(), m.getTarget(), + mb -> m.getAllTraits().values().forEach(mb::addTrait)); + } } + // Add ExpiresString cloning Expires' traits (so it reads the same header), retargeted. + b.addMember("ExpiresString", expiresStringId, + mb -> expires.getAllTraits().values().stream() + .filter(t -> !(t instanceof DocumentationTrait)) + .forEach(mb::addTrait)); + replacements.add(b.build()); } - // Add ExpiresString cloning Expires' traits (so it reads the same header), retargeted. - b.addMember("ExpiresString", expiresStringId, - mb -> expires.getAllTraits().values().stream() - .filter(t -> !(t instanceof DocumentationTrait)) - .forEach(mb::addTrait)); - replacements.add(b.build()); } return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } From e8ba45f3fa483818cf0af7b2e610dcb310ad1645 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 31 Aug 2026 13:42:43 -0400 Subject: [PATCH 28/44] Fix event stream dead code generation (unions, empty events) Smithy: EventStreamRenderer emits void() callback typedef for empty events Smithy: EventStreamRenderer emits arg-less default lambda for empty events Smithy: EventStreamRenderer dispatches empty events arg-less (no struct construct) Smithy: ShapeClassifier drops empty-member event structs from subObjects Smithy: drop incoming event-stream union header from subObjects/render Smithy: record event-stream empty-event cleanup as accepted parity divergence Smithy: skip empty-event struct include in event-stream handler header Smithy: mark event-stream handler-include defect resolved in parity-deltas --- docs/superpowers/plans/parity-deltas.md | 110 ----------- .../generators/model/ShapeClassifier.java | 27 ++- .../model/renderers/EventStreamRenderer.java | 167 ++++------------- .../model/EventStreamRendererTest.java | 176 +++++++++++++----- .../generators/model/ShapeClassifierTest.java | 62 ++++++ 5 files changed, 246 insertions(+), 296 deletions(-) delete mode 100644 docs/superpowers/plans/parity-deltas.md diff --git a/docs/superpowers/plans/parity-deltas.md b/docs/superpowers/plans/parity-deltas.md deleted file mode 100644 index 0aa51d260e8..00000000000 --- a/docs/superpowers/plans/parity-deltas.md +++ /dev/null @@ -1,110 +0,0 @@ -# Smithy Per-Service Model Parity — Documented Deltas - -Reviewed, accepted differences between Smithy-generated and C2J-generated `model::` -files. Anything not listed here must be resolved (empty diff) before a transform is "done". - -## Global-baseline deltas (not service-specific) -- **Stubbed payload serde (global):** Smithy emits empty serde bodies (e.g. `SerializePayload() const { return {}; }`, `OutputToStream(...) {}`) where C2J emits full serialization. Affects every shape; not service-specific. Pending serde implementation in the model plugin. -- **Doc-comment reflow (global):** Smithy wraps member documentation comments differently from C2J. Cosmetic; affects many members across services. - - -- **Pagination-traits files (global):** Smithy emits `PaginationTraits.h` under model/ that the C2J model tree doesn't; separate pagination plugin output, pre-existing. -- **ResponseMetadata standalone file (query/ec2, global):** C2J emits a standalone `ResponseMetadata.{h,cpp}`; the Smithy plugin injects ResponseMetadata via GlobalTransforms but doesn't emit it as a standalone sub-object file. Pre-existing; unaffected by the dual-role classifier fix. -- **Required-member HasBeenSet handling (FIXED):** The C++ SDK tracks member presence via `HasBeenSet`, not required-ness. In C2J, `CppClientGenerator#generateSourceFiles` unconditionally clears `required` on EVERY modeled member ("so we can do a value has been set check on all fields"), then `addRequestIdToResults` injects `ResponseMetadata` as required — so the injected `ResponseMetadata` is the ONLY member rendered with no `HasBeenSet()` getter + flag `= true` (in a `useRequiredField=true` context: sub-object/request; a pure result uses `useRequiredField=false` so even it inits `= false`). Every modeled member — plain `@required` (JSON) AND `@required @clientOptional` (the 17 query/xml services) — renders a getter + `= false`. The Smithy plugin does NOT mirror C2J's model mutation (that would discard `@required`, which serde/validation will want). Instead `MemberRenderer` keys the always-present treatment on **recognizing the injected `ResponseMetadata`** (member named `ResponseMetadata` whose target is the `ResponseMetadata` structure — exactly how C2J identifies it), gated on `emitHasBeenSet` (the `useRequiredField` proxy: `forStructure`=true, `forResult`=false), excepting event-stream / raw-streaming-payload members. `@required` is left intact on all members. `GlobalTransforms.injectResponseMetadata` fails fast (`IllegalStateException`) if a model already defines a `ResponseMetadata` shape or member, so the name-based recognition stays unambiguous (verified: 0 of 433 models define one). `GlobalTransforms.RESPONSE_METADATA` is the single shared name constant. Verified end-to-end on EC2 (identical to C2J): 747 results init `= false`, 4 dual-role sub-objects (`Reservation`/`Snapshot`/`Volume`/`VolumeAttachment`) init `= true`; `VolumeDetail.Size` (`@required @clientOptional`) → getter + `= false`. NOTE: keying on `@clientOptional` would be WRONG — absent from the 380 JSON services whose plain `@required` members C2J also treats as optional (e.g. DynamoDB `GetItemRequest.TableName`). -- **Enum Windows-macro #undef guard (FIXED):** C2J's `ModelEnumHeader.vm` wraps enum values that collide with a Windows preprocessor macro in `#if defined(_WIN32) && defined(X) / #undef X / #endif`, driven by `PlatformAndKeywordSanitizer.PREDEFINED_SYMBOLS_MAPPING` (namespace-keyed: `EC2→interface`, `DynamoDB→IN`, `S3Crt→IGNORE`). The Smithy `EnumRenderer.renderHeader` now emits the same guard (before the namespace block) via `predefinedWindowsSymbols(serviceNamespace, values)`, mirroring that mapping. Fixes e.g. `NetworkInterfaceType.h` (`interface` value). Per-service/namespace keyed; other services with the same value do not emit it. -- **ShapeClassifier dual-role fix (FIXED):** structures that are both an operation output AND a member target (e.g. lambda FunctionConfiguration/AliasConfiguration/EventSourceMappingConfiguration/Concurrency/FunctionEventInvokeConfig) are now emitted as sub-objects too, matching C2J. Verified: lambda Only-in-C2J model files 10 -> 0; no spurious over-emission; sqs unchanged. -- **Deprecated-orphan dead files (global, ACCEPTED):** When a shape is reachable ONLY through `@deprecated` member(s), C2J still emits a model file for it, while the Smithy plugin omits it. Root cause is a C2J bug: `C2jModelToGeneratorModelTransformer.removeUnreferencedShapes()` is a single, non-transitive pass over `referencedBy`, so it removes only the first-order orphan (usually the intermediate list/map, which emits no file) and still emits the struct/enum that list pointed at as a dead, unreferenced file. The Smithy plugin's `computeReachableShapes` walks only surviving edges and correctly omits the whole orphan subtree. **Proven safe:** across all 433 services this drops files in 17 (e.g. ec2: AssociatedTargetNetwork/AssociatedNetworkType/ElasticGpuSpecification/ElasticInferenceAccelerator/LaunchTemplateElasticInferenceAccelerator; guardduty: 32) with **0 dangling references** — a shape shared with any non-deprecated reference is always kept (verified by `GlobalTransformsTest.dropDeprecatedMembers_sharedTargetSurvivesViaNonDeprecatedReference`). Smithy output is strictly cleaner; accepted rather than replicating C2J's dead files. - -## rds -- SourceRegion member injected by SourceRegionTransform is present and structurally identical to C2J at the member/accessor level (verified Task 2). Remaining rds diffs are the two global deltas above. - -## docdb -_(none yet)_ - -## neptune -_(none yet)_ - -## lambda -_(none yet)_ - -## sqs -_(none yet)_ - -## apigateway -_(none yet)_ - -## apigatewayv2 -_(none yet)_ - -## ec2 -- Result naming: operation-output result classes use `Response` via ResultRenderer+ShapeUtil.getResultSuffix; nested `*Result` domain structs renamed to `*Response` by Ec2Transforms. Verified 0 Result/Response file mismatches vs C2J. -- SpotInstanceState `disabled` value present (parity). -- SecureBlobAttributeValue (FIXED via Ec2Transforms): upstream `aws/aws-models` itself diverges — the C2J `ec2//service-2.json` models `ModifyInstanceAttributeRequest.UserData -> SecureBlobAttributeValue -> SecureBlob(@sensitive)`, but the upstream Smithy `ec2/smithy/model.json` still targets the non-sensitive `BlobAttributeValue` (verified against upstream on master). Re-syncing the Smithy model would NOT fix it (upstream Smithy lacks the shape). `Ec2Transforms.addSecureBlobUserData` mirrors the C2J modeling in the Smithy model at generation time: adds `SecureBlob`(@sensitive -> CryptoBuffer) + `SecureBlobAttributeValue{Value}` and repoints `UserData`, which orphans `BlobAttributeValue` so it drops from the emitted set exactly as in C2J. Self-retires (no-op) once the upstream Smithy model catches up. Temporary compensation for upstream data lag; the durable fix is an upstream aws-models correction. Note: the generated `SecureBlobAttributeValue.{h,cpp}` still differs from C2J only in the stubbed-serde bodies (global delta above). -- Deprecated-orphan dead files: 5 shapes / 10 files (AssociatedTargetNetwork, AssociatedNetworkType, ElasticGpuSpecification, ElasticInferenceAccelerator, LaunchTemplateElasticInferenceAccelerator) — see global "Deprecated-orphan dead files" delta above. -- OUT OF SCOPE (remain C2J, documented): ~180 legacy error-code injection, CopySnapshot presign, custom endpoint-enum template. These are client/error/endpoint artifacts, not model-shape. -- Remaining diffs are the two global deltas (stubbed serde, doc reflow). - -## S3 accepted divergences -- **Expires deprecation note on both GetObjectResult AND HeadObjectResult:** The Smithy path applies - the `Expires` "Deprecated: Please use ExpiresString instead." doc-comment note to every operation - output that carries `Expires`, so it appears on both `GetObjectResult` and `HeadObjectResult`. C2J - emits the note only on `GetObjectResult` — an artifact of C2J deduping the customization across a - `Set`, which collapses the shared `Expires` member so the note lands on just one - result. The Smithy behavior is intentional and more consistent (both results describe the same - deprecated field identically). This is doc-comment-only and non-structural — accessors, member - order, and wire behavior are unchanged — so it is accepted rather than replicating C2J's - dedup-driven omission. No transform change (`addExpiresCustomization` is unchanged). - -## S3 serde-phased customizations (deferred until Smithy serde lands) -- `markChecksumMembers` (S3 `CHECKSUM_MEMBERS_ENUMS`): checksum members drive request serialization - only; no model-header delta today. Implement as an `S3Transforms` marker step when serde is - un-stubbed. Map (member → algorithm value): ChecksumCRC32→CRC32, ChecksumCRC32C→CRC32C, - ChecksumSHA1→SHA1, ChecksumSHA256→SHA256, ChecksumSHA512→SHA512, ChecksumXXHASH64→XXHASH64, - ChecksumXXHASH3→XXHASH3, ChecksumXXHASH128→XXHASH128, ChecksumMD5→MD5. (ChecksumCRC64NVME NOT mapped.) -- `injectAccessLogTagQuery` (S3 `customizedAccessLogTag` querystring map on every request): - **IMPLEMENTED** (Task 7) — `S3Transforms.injectAccessLogTagQuery` injects a `customizedAccessLogTag` - `map` member (targeting `com.amazonaws.s3#CustomizedAccessLogTag`, key+value - `smithy.api#String`) onto every operation request shape, appended last, idempotent. This closes the - `.h` member-accessor delta (`GetCustomizedAccessLogTag` / `SetCustomizedAccessLogTag` / - `WithCustomizedAccessLogTag` / `AddCustomizedAccessLogTag` / `m_customizedAccessLogTag`). The - querystring binding (`location=querystring`, `customizedQuery=true` → `AddQueryStringParameters` - serde) is still DEFERRED until Smithy serde lands; no `@httpQuery`/`@httpQueryParams` trait is - attached yet, to avoid perturbing stubbed request emission. - -### Task 7 investigation note (evidence correction — access-log IMPLEMENTED, checksum DEFERRED) -The two "serde only" labels above are imprecise: **both customizations DO produce an observable -model-HEADER delta today** in C2J vs the current `--use-smithy-models` output. Per a later controller -decision, the **access-log tag member injection is now IMPLEMENTED** (the `.h` accessors are a pure -model-shape delta, closeable by a standalone `S3Transforms` injection; only its querystring serde -binding is deferred). **Checksum stays DEFERRED** — it is not closeable by a marker transform without -renderer work. Evidence: - -- **Checksum (header delta, needs MemberRenderer support):** `markChecksumMembers` sets - `ShapeMember.checksumMember/checksumEnumMember`, which C2J's *header* template - `ModelClassMembersAndInlines.vm` (lines 56–59, 100–101) consumes to emit a setter side-effect — - e.g. `SetChecksumCRC32(...)` also calls `SetChecksumAlgorithm(ChecksumAlgorithm::CRC32);` (and a - `const char*` overload). Confirmed present in C2J `generated/.../PutObjectRequest.h` - (`SetChecksumCRC32/CRC32C/SHA1/SHA256/SHA512/MD5/XXHASH64/XXHASH3/XXHASH128`), and correctly ABSENT - on the unmapped `ChecksumCRC64NVME`. The Smithy `MemberRenderer` setter bodies emit only - `HasBeenSet = true; ...` and never `SetChecksumAlgorithm(...)`; `RequestRenderer.renderChecksumImpls` - only handles the separate `@httpChecksum` trait impls (`GetChecksumAlgorithmName`, `ChecksumAlgorithmIsSet`, - etc. — the `ModelClassChecksumMembers.vm` concern), not the value-member setter side-effect. So a - *marker* transform alone is inert: closing this delta requires `MemberRenderer` to grow bespoke - logic that reads the marker. Deferred to the render/serde phase. - -- **Access-log tag (header delta — IMPLEMENTED; query-binding serde deferred):** - `injectAccessLogTagQuery` (S3RestXmlCppClientGenerator.java ~299–340) injects a real - `customizedAccessLogTag` `map` member (`location=querystring`, - `customizedQuery=true`) into EVERY request input. C2J's header template renders full accessors — - confirmed in C2J `generated/.../PutObjectRequest.h` and others (`GetCustomizedAccessLogTag`, - `SetCustomizedAccessLogTag`, `WithCustomizedAccessLogTag`, `AddCustomizedAccessLogTag`, - `m_customizedAccessLogTag`, `m_customizedAccessLogTagHasBeenSet`). The Smithy S3 model - (`smithy/api-descriptions/s3.json`) has ZERO occurrences. `S3Transforms.injectAccessLogTagQuery` - now mirrors the C2J injection at the model-shape level: it appends the `customizedAccessLogTag` - `map` member to every request input (idempotent), closing the `.h` accessor delta. - The query-string binding is intentionally NOT modeled yet: no `@httpQuery`/`@httpQueryParams` trait - is attached, because that would engage the (stubbed) serde/render path and risk perturbing request - emission. For restXml an unbound member would be misclassified as a payload member during serde; - the correct query-param binding (`customizedQuery` loop in `AddQueryStringParametersToRequest.vm`) - lands with the querystring serde work. Byte-parity of the querystring serialization is a Task 9 - follow-up once Smithy serde is un-stubbed. diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java index 3f7c035a93b..353b2cbce3e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java @@ -181,6 +181,16 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto } } + // Incoming event-stream union shape ids: the @streaming union member of every operation + // output collected as an event-stream handler. These unions are realized via the handler + // (EventStreamRenderer.renderHandler{Header,Source}) and never referenced as a data type, + // so their standalone .h is dead public API that we omit. + Set incomingEventStreamUnionIds = new HashSet<>(); + for (EventStreamInfo info : eventStreamHandlers) { + streamingUnionMember(info.resultShape(), model) + .ifPresent(u -> incomingEventStreamUnionIds.add(u.getId())); + } + // Walk all reachable shapes and classify remaining ones for (ShapeId id : reachable) { Shape shape = model.expectShape(id); @@ -200,10 +210,19 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto subObjects.add(shape); } } else if (shape.isStructureShape() || shape.isUnionShape()) { - // A shape marked @customRendered is emitted by a dedicated renderer (e.g. - // DynamoDbRenderer for AttributeValue); skip the default sub-object emission so the - // two do not both write — and append into — the same model file. - if (!shape.hasTrait(CustomRenderedTrait.class)) { + // Skip: + // - @customRendered shapes (emitted by a dedicated renderer, e.g. DynamoDbRenderer + // for AttributeValue) so the two do not both write — and append into — the same + // model file. + // - empty-member event structs — after EventStreamRenderer's void() callback fix, + // these have no other references and shipping their .h/.cpp adds dead public API + // to the SDK. + boolean customRendered = shape.hasTrait(CustomRenderedTrait.class); + boolean emptyEventStruct = eventStructIds.contains(id) + && shape.isStructureShape() + && shape.members().isEmpty(); + boolean incomingUnion = incomingEventStreamUnionIds.contains(id); + if (!customRendered && !emptyEventStruct && !incomingUnion) { subObjects.add(shape); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java index f256588f006..0ffab02d1c0 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java @@ -4,22 +4,18 @@ */ package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; -import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; -import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppNames; import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.protocol.FileKind; import com.amazonaws.util.awsclientsmithygenerator.generators.model.protocol.ProtocolTraits; import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; -import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.EventStreamInfo; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.UnionShape; -import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.model.traits.StreamingTrait; @@ -29,9 +25,11 @@ /** * Renders C++ event stream artifacts for response-side (simplex) streaming operations: - * the handler, initial response, and event stream union. Driven by the classifier's - * {@link EventStreamInfo} list. Event structure shapes themselves are generated - * elsewhere (as reachable sub-objects) and only referenced here. + * the handler and initial response. Driven by the classifier's {@link EventStreamInfo} + * list. Event structure shapes themselves are generated elsewhere (as reachable + * sub-objects) and only referenced here. The {@code @streaming} union data type is not + * emitted: it is realized entirely through the handler and referenced by nothing, so the + * classifier drops it from sub-objects (dead public API). * *

No protocol-specific serialization is emitted; payload (de)serialization points * are protocol-agnostic TODO stubs via {@link ProtocolTraits}. @@ -61,7 +59,6 @@ public void render(CppWriterDelegator writerDelegator) { renderHandlerHeader(writerDelegator, info.operationName(), events); renderHandlerSource(writerDelegator, info.operationName(), events); renderInitialResponse(writerDelegator, info.operationName(), info.resultShape()); - renderEventStreamUnion(writerDelegator, info.operationName(), union, events, exceptions); } } @@ -93,21 +90,6 @@ private String eventShapeName(MemberShape member) { return member.getTarget().getName(); } - /** - * True if an exception member targets a modeled exception (members beyond the trivial - * message/code). C2J types modeled exceptions as their concrete shape (with a model include) - * and non-modeled ones as the generic {@code Error} wrapper. - */ - private boolean isModeledException(MemberShape exc) { - StructureShape target = ctx.model().expectShape(exc.getTarget(), StructureShape.class); - return ShapeClassifier.isModeledException(target, ctx.protocolTraits().protocol()); - } - - /** The C++ type for an exception member: concrete shape name if modeled, else {@code errorType}. */ - private String exceptionType(MemberShape exc, String errorType) { - return isModeledException(exc) ? exc.getTarget().getName() : errorType; - } - /** The wire member key, e.g. "alpha". */ private String wireKey(MemberShape member) { return member.getMemberName(); @@ -133,7 +115,10 @@ private void renderHandlerHeader(CppWriterDelegator writerDelegator, String opNa writer.write("#include ", ctx.smithyServiceName(), ctx.namespace()); writer.write("#include ", ctx.smithyServiceName(), opName); for (MemberShape event : events) { - writer.write("#include ", ctx.smithyServiceName(), eventShapeName(event)); + boolean emptyEvent = ctx.model().expectShape(event.getTarget()).members().isEmpty(); + if (!emptyEvent) { + writer.write("#include ", ctx.smithyServiceName(), eventShapeName(event)); + } } writer.write(""); ModelFile.modelNamespace(writer, ctx.namespace(), () -> { @@ -154,7 +139,12 @@ private void renderHandlerHeader(CppWriterDelegator writerDelegator, String opNa writer.write("typedef std::function $1LInitialResponseCallbackEx;", opName); for (MemberShape event : events) { String ev = eventShapeName(event); - writer.write("typedef std::function $1LCallback;", ev); + boolean emptyEvent = ctx.model().expectShape(event.getTarget()).members().isEmpty(); + if (emptyEvent) { + writer.write("typedef std::function $1LCallback;", ev); + } else { + writer.write("typedef std::function $1LCallback;", ev); + } } writer.write("typedef std::function& error)> ErrorCallback;", ctx.namespace()); writer.write(""); @@ -253,9 +243,16 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa }); for (MemberShape event : events) { String ev = eventShapeName(event); - writer.openBlock("m_on$1L = [&](const $1L&) {", "};", ev, () -> { - writer.write("AWS_LOGSTREAM_TRACE($1L, \"$2L received.\");", tag, ev); - }); + boolean emptyEvent = ctx.model().expectShape(event.getTarget()).members().isEmpty(); + if (emptyEvent) { + writer.openBlock("m_on$1L = [&]() {", "};", ev, () -> { + writer.write("AWS_LOGSTREAM_TRACE($1L, \"$2L received.\");", tag, ev); + }); + } else { + writer.openBlock("m_on$1L = [&](const $1L&) {", "};", ev, () -> { + writer.write("AWS_LOGSTREAM_TRACE($1L, \"$2L received.\");", tag, ev); + }); + } } writer.openBlock("m_onError = [&](const AWSError<$1LErrors>& error) {", "};", ctx.namespace(), () -> { writer.write("AWS_LOGSTREAM_TRACE($1L, \"$2L Errors received, \" << error);", tag, ctx.namespace()); @@ -309,8 +306,13 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa }); for (MemberShape event : events) { writer.openBlock("case $1LEventType::$2L: {", "}", opName, enumConstant(event), () -> { - ctx.protocolTraits().writeEventPayloadDecode(writer, eventShapeName(event), - "m_on" + eventShapeName(event)); + boolean emptyEvent = ctx.model().expectShape(event.getTarget()).members().isEmpty(); + String callbackMember = "m_on" + eventShapeName(event); + if (emptyEvent) { + writer.write("$L();", callbackMember); + } else { + ctx.protocolTraits().writeEventPayloadDecode(writer, eventShapeName(event), callbackMember); + } writer.write("break;"); }); } @@ -408,7 +410,7 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa }); } - // ---- Initial response / event stream union ------------------------------ + // ---- Initial response --------------------------------------------------- /** * Builds a synthetic {@code InitialResponse} structure from the result's non-event-stream @@ -500,107 +502,4 @@ private void renderInitialResponse(CppWriterDelegator writerDelegator, String op }); } - private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String opName, - UnionShape union, List events, - List exceptions) { - String className = union.getId().getName(); - String errorType = ctx.namespace() + "Error"; - - String headerFile = "include/aws/" + ctx.smithyServiceName() + "/model/" + className + ".h"; - writerDelegator.useFileWriter(headerFile, writer -> { - writer.write("#pragma once"); - writer.write("#include ", ctx.smithyServiceName(), ctx.namespace()); - // C2J omits the service Errors include: its header-include computation skips non-modeled - // exception members (CppViewHelper: `if (next.isException() && !next.isModeledException()) - // continue;`), which are the generic Error wrapper and resolve transitively. - // Concrete event shapes AND modeled exceptions get their own model include. - for (MemberShape event : events) { - writer.write("#include ", ctx.smithyServiceName(), eventShapeName(event)); - } - for (MemberShape exc : exceptions) { - if (isModeledException(exc)) { - writer.write("#include ", ctx.smithyServiceName(), exc.getTarget().getName()); - } - } - writer.write(""); - writer.write("#include "); - writer.write(""); - ModelFile.modelNamespace(writer, ctx.namespace(), - () -> ctx.protocolTraits().writeShapeForwardDeclarations(writer), - () -> { - writer.write(""); - MemberRenderer.renderClassDocComment(writer, union, ctx.smithyServiceName(), ctx.service().getVersion()); - writer.openBlock("class $L {", "};", className, () -> { - writer.write("public:"); - ctx.protocolTraits().writeSerdeMethodDecls(writer, ctx.exportMacro(), className, null); - writer.write(""); - // Event member accessors, typed as their concrete shape. - for (MemberShape event : events) { - String cppType = CppTypeMapper.getCppType(ctx.model().expectShape(event.getTarget()), ctx.model()); - renderShapeAccessor(writer, className, cppType, event.getMemberName(), event); - } - // Exception member accessors: modeled -> concrete type; non-modeled -> Error. - for (MemberShape exc : exceptions) { - renderShapeAccessor(writer, className, exceptionType(exc, errorType), exc.getMemberName(), exc); - } - writer.dedent(); - writer.write("private:"); - writer.indent(); - // Data members - for (MemberShape event : events) { - String cppType = CppTypeMapper.getCppType(ctx.model().expectShape(event.getTarget()), ctx.model()); - writer.write("$1L $2L;", cppType, CppNames.fieldName(event.getMemberName())); - } - for (MemberShape exc : exceptions) { - writer.write("$1L $2L;", exceptionType(exc, errorType), CppNames.fieldName(exc.getMemberName())); - } - // HasBeenSet flags - for (MemberShape event : events) { - writer.write("bool $1LHasBeenSet = false;", CppNames.fieldName(event.getMemberName())); - } - for (MemberShape exc : exceptions) { - writer.write("bool $1LHasBeenSet = false;", CppNames.fieldName(exc.getMemberName())); - } - }); - writer.write(""); - }); - }); - - // C2J generates the event stream union as a header-only type: the serde methods are - // declared but never defined or referenced (the handler dispatches on the concrete - // event shape, not the union). No .cpp is emitted, to match mainline parity. - } - - /** - * Renders a Get/HasBeenSet/Set/With accessor block for a union member, typed by the - * given C++ type string. Used for both event members (concrete shape type) and - * exception members (the service error wrapper). Mirrors the templated setter style - * used by MemberRenderer. - */ - private void renderShapeAccessor(CppWriter writer, String className, String cppType, String memberName, - MemberShape member) { - String getter = CppNames.capitalize(memberName); - String field = CppNames.fieldName(memberName); - String templateParam = getter + "T"; - writer.write("///@{"); - if (member.getTrait(DocumentationTrait.class).isPresent()) { - MemberRenderer.writeDocComment(writer, - MemberRenderer.collapseWhitespace(member.getTrait(DocumentationTrait.class).get().getValue())); - } else { - writer.write(""); - } - writer.write("inline const $1L& Get$2L() const { return $3L; }", cppType, getter, field); - writer.write("inline bool $1LHasBeenSet() const { return $2LHasBeenSet; }", getter, field); - writer.write("template ", templateParam, cppType); - writer.openBlock("void Set$1L($2L&& value) {", "}", getter, templateParam, () -> { - writer.write("$1LHasBeenSet = true;", field); - writer.write("$1L = std::forward<$2L>(value);", field, templateParam); - }); - writer.write("template ", templateParam, cppType); - writer.openBlock("$1L& With$2L($3L&& value) {", "}", className, getter, templateParam, () -> { - writer.write("Set$1L(std::forward<$2L>(value));", getter, templateParam); - writer.write("return *this;"); - }); - writer.write("///@}"); - } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java index f3638429c04..d04d8a98092 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java @@ -87,8 +87,61 @@ private static Model twoEventModel() { return Model.builder().addShapes(str, stream, eventA, eventB, exc, modeledExc, input, output, op, service).build(); } + // A @streaming union with one empty event (target shape has no modeled members) and one data + // event (target shape has a modeled member). Callback/member names derive from the target + // shape name, matching twoEventModel's convention (alpha -> AlphaEvent -> m_onAlphaEvent). + private static Model unionWithEmptyAndDataEvent() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape emptyEvent = StructureShape.builder() + .id("com.example#EmptyEvent") + .build(); + StructureShape dataEvent = StructureShape.builder() + .id("com.example#DataEvent") + .addMember("data", str.getId()) + .build(); + UnionShape stream = UnionShape.builder() + .id("com.example#MyStreamEventStream") + .addTrait(new StreamingTrait()) + .addMember("emptyEvent", emptyEvent.getId()) + .addMember("dataEvent", dataEvent.getId()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoStreamInput") + .addMember("name", str.getId()) + .build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoStreamOutput") + .addMember(software.amazon.smithy.model.shapes.MemberShape.builder() + .id("com.example#DoStreamOutput$stream").target(stream.getId()) + .addTrait(new software.amazon.smithy.model.traits.HttpPayloadTrait()).build()) + .build(); + software.amazon.smithy.model.shapes.OperationShape op = + software.amazon.smithy.model.shapes.OperationShape.builder() + .id("com.example#DoStream") + .input(input.getId()) + .output(output.getId()) + .build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example") + .version("2024-01-01") + .addOperation(op.getId()) + .build(); + return Model.builder().addShapes(str, stream, emptyEvent, dataEvent, input, output, op, service).build(); + } + private static String render(String fileSuffix) { - Model model = twoEventModel(); + return render(twoEventModel(), fileSuffix); + } + + private static String renderHandlerHeaderFor(Model model) { + return render(model, "DoStreamHandler.h"); + } + + private static String renderHandlerSourceFor(Model model) { + return render(model, "DoStreamHandler.cpp"); + } + + private static String render(Model model, String fileSuffix) { ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); MockManifest manifest = new MockManifest(); CppWriterDelegator delegator = new CppWriterDelegator(manifest); @@ -106,6 +159,22 @@ private static String render(String fileSuffix) { .orElseThrow(); } + private static java.util.List renderedFilePaths(Model model) { + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + Protocol protocol = ProtocolResolver.resolve(service, model); + EventStreamRenderer renderer = new EventStreamRenderer( + ShapeClassifier.classify(model, service, protocol).eventStreamHandlers(), + new RenderContext(model, service, ProtocolResolver.traitsFor(protocol), + "Example", "AWS_EXAMPLE_API", "example")); + renderer.render(delegator); + delegator.flushWriters(); + return manifest.getFiles().stream() + .map(java.nio.file.Path::toString) + .collect(java.util.stream.Collectors.toList()); + } + @Test void handlerHeader_hasEnumAndCallbacksPerEvent() { String h = render("DoStreamHandler.h"); @@ -138,53 +207,19 @@ void handlerHeader_wrapsInitialResponseSettersInDocGroup() { } @Test - void eventStreamUnionHeader_typesEventAndExceptionMembers() { - String h = render("MyStreamEventStream.h"); - // NOTE: file name derives from the UNION shape name (MyStream), not the operation. - assertTrue(h.contains("class MyStreamEventStream"), "Missing union class: " + h); - // Event member typed as its concrete shape - assertTrue(h.contains("const AlphaEvent& GetAlpha()") || h.contains("GetAlpha"), - "Missing event accessor: " + h); - // Exception member typed as Error - assertTrue(h.contains("ExampleError"), "Exception members must be typed as ExampleError: " + h); - } - - @Test - void eventStreamUnionHeader_modeledExceptionUsesConcreteTypeAndInclude() { - // C2J types a modeled exception member (extra members beyond message/code) as its concrete - // shape and includes its model header, while a non-modeled exception stays the generic - // Error (no include). Matches CppViewHelper's isException/isModeledException gate. - String h = render("MyStreamEventStream.h"); - // Modeled exception -> concrete type + include. - assertTrue(h.contains("const DetailedException& GetDetailedException()"), - "Modeled exception must use its concrete type: " + h); - assertTrue(h.contains("#include "), - "Modeled exception must bring its model include: " + h); - // Non-modeled exception -> generic ExampleError. - assertTrue(h.contains("const ExampleError& GetBadException()"), - "Non-modeled exception must use the generic error type: " + h); - } - - @Test - void eventStreamUnionHeader_omitsServiceErrorsInclude() { - // C2J's computeHeaderIncludes skips the model include for non-modeled exception members - // (CppViewHelper: `if (next.isException() && !next.isModeledException()) continue;`). - // The union's exception members are the generic ExampleError wrapper, so the union header - // must NOT include the service Errors header — it resolves transitively. The handler - // header (a separate file) still includes it. - String h = render("MyStreamEventStream.h"); - assertFalse(h.contains("#include "), - "Union header must not include the service Errors header: " + h); - } - - @Test - void eventStreamUnionHeader_rendersClassAndMemberDocs() { - String h = render("MyStreamEventStream.h"); - // Union class-level documentation + See Also link. - assertTrue(h.contains("Tagged union of stream events."), "Missing union class doc: " + h); - assertTrue(h.contains("See Also:"), "Missing See Also block on union class: " + h); - // Member-level doc flows to the accessor for the member that has one. - assertTrue(h.contains("Alpha event doc."), "Missing alpha member doc: " + h); + void eventStreamUnionHeader_noLongerEmitted() { + // The incoming event-stream union is realized via the handler; nothing references it as a + // data type. The renderer must not emit its standalone .h (dead public API) — the + // classifier already drops it from subObjects so no other renderer emits it either. + java.util.List paths = renderedFilePaths(twoEventModel()); + assertTrue(paths.stream().noneMatch(p -> p.endsWith("MyStreamEventStream.h")), + "incoming event-stream union header must not be emitted: " + paths); + // And no rendered file declares the union class. + for (String path : paths) { + String contents = render(twoEventModel(), path.substring(path.lastIndexOf('/') + 1)); + assertFalse(contents.contains("class MyStreamEventStream"), + "no rendered file may declare the union class: " + path); + } } @Test @@ -284,4 +319,49 @@ void handlerSource_eventCasesUseStubNoProtocolTokens() { assertFalse(c.contains("JsonValue"), "No protocol tokens in handler: " + c); assertFalse(c.contains("Cbor"), "No protocol tokens in handler: " + c); } + + @Test + void emptyEventEmitsVoidCallbackTypedef() { + String out = renderHandlerHeaderFor(unionWithEmptyAndDataEvent()); + assertTrue(out.contains("typedef std::function EmptyEventCallback;"), + "empty event => arg-less typedef: " + out); + assertTrue(out.contains("typedef std::function DataEventCallback;"), + "data event => struct-arg typedef preserved: " + out); + } + + @Test + void emptyEventEmitsArglessDefaultLambda() { + String out = renderHandlerSourceFor(unionWithEmptyAndDataEvent()); + assertTrue(out.contains("m_onEmptyEvent = [&]() {"), + "empty event default lambda takes no args: " + out); + assertTrue(out.contains("m_onDataEvent = [&](const DataEvent&) {"), + "data event default lambda unchanged: " + out); + } + + @Test + void emptyEventDispatchesWithoutConstructingStruct() { + String out = renderHandlerSourceFor(unionWithEmptyAndDataEvent()); + assertTrue(out.contains("m_onEmptyEvent();"), + "empty event dispatched arg-less: " + out); + assertFalse(out.contains("m_onEmptyEvent(EmptyEvent{"), + "empty event dispatch must not construct the empty struct: " + out); + } + + @Test + void nonEmptyEventBehaviorUnchanged() { + String out = renderHandlerHeaderFor(unionWithEmptyAndDataEvent()); + assertTrue(out.contains("typedef std::function DataEventCallback;"), + "non-empty event typedef unchanged: " + out); + } + + @Test + void emptyEventStructHeaderNotIncluded() { + // An empty event's struct is dropped by the classifier, so including its header would dangle + // and is unnecessary (the callback is arg-less). A data event keeps its struct include. + String out = renderHandlerHeaderFor(unionWithEmptyAndDataEvent()); + assertFalse(out.contains("#include "), + "empty event struct header must not be included: " + out); + assertTrue(out.contains("#include "), + "data event struct header must still be included: " + out); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java index 946557260de..3ae0b5d93c2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java @@ -617,6 +617,68 @@ void customRenderedShape_isExcludedFromSubObjects() { "unmarked shape must remain a sub-object: " + classified.subObjects()); } + /** + * A @streaming union with two event members: one empty-member event (like S3's + * ContinuationEvent / EndEvent, referenced by nothing after EventStreamRenderer's void() + * callback fix) and one data event carrying a string member. Bound to an operation output so + * both event structs are reachable. + */ + private Model eventStreamEmptyAndDataModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape emptyEvt = StructureShape.builder() + .id("com.example#EmptyEvt") + .build(); + StructureShape dataEvt = StructureShape.builder() + .id("com.example#DataEvt") + .addMember("records", str.getId()) + .build(); + UnionShape eventStream = UnionShape.builder() + .id("com.example#SelectEventStream") + .addTrait(new StreamingTrait()) + .addMember("Empty", emptyEvt.getId()) + .addMember("Data", dataEvt.getId()) + .build(); + StructureShape request = StructureShape.builder() + .id("com.example#SelectRequest").addMember("name", str.getId()).build(); + StructureShape response = StructureShape.builder() + .id("com.example#SelectResponse").addMember("events", eventStream.getId()).build(); + OperationShape op = OperationShape.builder() + .id("com.example#Select").input(request.getId()).output(response.getId()).build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2023-01-01").addOperation(op.getId()) + .addTrait(ServiceTrait.builder().sdkId("test").arnNamespace("test").cloudFormationName("Test").cloudTrailEventSource("test").build()) + .build(); + return Model.builder() + .addShapes(str, emptyEvt, dataEvt, eventStream, request, response, op, service) + .build(); + } + + @Test + void classifyDropsEmptyMemberEventStructFromSubObjects() { + Model model = eventStreamEmptyAndDataModel(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + assertFalse(classified.subObjects().stream() + .anyMatch(s -> s.getId().getName().equals("EmptyEvt")), + "empty event struct dropped from subObjects: " + classified.subObjects()); + assertTrue(classified.subObjects().stream() + .anyMatch(s -> s.getId().getName().equals("DataEvt")), + "data event struct still emitted as sub-object: " + classified.subObjects()); + } + + @Test + void classifyDropsIncomingEventStreamUnionFromSubObjects() { + // The @streaming union bound to an operation output (an incoming event stream) is realized + // via the generated handler; nothing references the union as a data type, so it must not be + // emitted as a standalone sub-object header. + Model model = eventStreamEmptyAndDataModel(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + assertFalse(classified.subObjects().stream() + .anyMatch(s -> s.getId().getName().equals("SelectEventStream")), + "incoming event-stream union dropped from subObjects: " + classified.subObjects()); + } + @Test void classifiesEnumShape() { // StringShape with @enum trait -> classified as enum From c4991fac8a9be601c1fae716acb667beee9197c3 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 31 Aug 2026 15:08:43 -0400 Subject: [PATCH 29/44] Smithy: restore S3 request AddQueryStringParameters and HasEmbeddedError methods - injectAccessLogTagQuery: mark customizedAccessLogTag @httpQueryParams so RestXml emits AddQueryStringParameters (was missing on 110 requests) - new EmbeddedErrorsTrait + S3Transforms.markEmbeddedErrors (C2J functionsWithEmbeddedErrors set) + RestXmlProtocolTraits emits HasEmbeddedError under the marker (was missing on 92 requests) --- .../model/protocol/ProtocolTraits.java | 22 ++++++ .../model/protocol/RestXmlProtocolTraits.java | 9 +++ .../model/transforms/EmbeddedErrorsTrait.java | 26 +++++++ .../model/transforms/S3Transforms.java | 68 ++++++++++++++++++- .../model/protocol/XmlProtocolTraitsTest.java | 29 ++++++++ .../model/transforms/S3TransformsTest.java | 24 +++++++ 6 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java index d7002148dc0..001020ce84d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java @@ -190,6 +190,28 @@ default void writeGetRequestSpecificHeadersImpl(CppWriter writer, String classNa }); } + /** + * Declares the {@code HasEmbeddedError} override C2J emits under + * {@code #if($shape.hasEmbeddedErrors())} in {@code RequestHeader.vm}. Text matches C2J exactly + * (unqualified {@code IOStream} / {@code Http::HeaderValueCollection}, resolved via the request + * header's {@code Aws} usings). Only S3 requests carry {@code EmbeddedErrorsTrait}, so the + * caller gates emission on that marker. + */ + default void writeHasEmbeddedErrorDecl(CppWriter writer, String exportMacro) { + writer.write("$L bool HasEmbeddedError(IOStream &body, " + + "const Http::HeaderValueCollection &header) const override;", exportMacro); + } + + /** + * Emits a stubbed {@code HasEmbeddedError} body. The real XML error-sniff is a serde concern + * deferred plugin-wide; returning {@code false} with unnamed params keeps the override present + * (byte-parity for the request header) without tripping {@code -Werror} on unused parameters. + */ + default void writeHasEmbeddedErrorImpl(CppWriter writer, String className) { + writer.write("bool $L::HasEmbeddedError(Aws::IOStream&, " + + "const Aws::Http::HeaderValueCollection&) const { return false; }", className); + } + default void writeAddQueryStringParametersImpl(CppWriter writer, String className) { writer.openBlock("void $L::AddQueryStringParameters(Aws::Http::URI& uri) const {", "}", className, () -> { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java index 4eda33f32fd..54b03cf52ac 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java @@ -6,6 +6,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.EmbeddedErrorsTrait; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -148,6 +149,10 @@ public void writeRequestMethodDecls(CppWriter writer, String exportMacro, writer.write(""); writeAddQueryStringParametersDecl(writer, exportMacro); } + if (shape.hasTrait(EmbeddedErrorsTrait.class)) { + writer.write(""); + writeHasEmbeddedErrorDecl(writer, exportMacro); + } } @Override @@ -165,5 +170,9 @@ public void writeRequestMethodImpls(CppWriter writer, String className, writer.write(""); writeAddQueryStringParametersImpl(writer, className); } + if (shape.hasTrait(EmbeddedErrorsTrait.class)) { + writer.write(""); + writeHasEmbeddedErrorImpl(writer, className); + } } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java new file mode 100644 index 00000000000..51fba711240 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java @@ -0,0 +1,26 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link S3Transforms} on each S3 + * request structure that C2J's {@code S3RestXmlCppClientGenerator} lists in its hardcoded + * {@code functionsWithEmbeddedErrors} set ({@code shape.setEmbeddedErrors(true)}). REST-XML request + * rendering turns the marker into the {@code HasEmbeddedError(IOStream&, HeaderValueCollection&)} + * override that {@code RequestHeader.vm} emits under {@code #if($shape.hasEmbeddedErrors())}, so + * these S3 requests match C2J. Kept as a marker + generic renderer rule (not a service-name + * {@code if}) so the renderer stays service-agnostic; only S3 requests ever carry it. + */ +public final class EmbeddedErrorsTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#embeddedErrors"); + + public EmbeddedErrorsTrait() { + super(ID, Node.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 1c3aa9c7993..6597d5f5924 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -24,6 +24,7 @@ import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.HttpHeaderTrait; +import software.amazon.smithy.model.traits.HttpQueryParamsTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.transform.ModelTransformer; import java.util.ArrayList; @@ -55,8 +56,67 @@ private static Model apply(Model model, ServiceShape service) { if (!"s3".equals(name) && !"s3-crt".equals(name)) { return model; } - return injectAccessLogTagQuery(normalizeReplicationStatus(expandBucketLocationConstraint( - hackGetObjectResult(addExpiresCustomization(renameCopyObjectResult(model, service), service)))), service); + return markEmbeddedErrors(injectAccessLogTagQuery(normalizeReplicationStatus( + expandBucketLocationConstraint(hackGetObjectResult( + addExpiresCustomization(renameCopyObjectResult(model, service), service)))), service)); + } + + // C2J's S3RestXmlCppClientGenerator carries a hardcoded functionsWithEmbeddedErrors set; each + // listed request shape gets shape.setEmbeddedErrors(true), which RequestHeader.vm turns into the + // HasEmbeddedError(...) override. Mirror that by stamping EmbeddedErrorsTrait on every request + // structure whose simple name is in the set; REST-XML request rendering emits the method for + // marker-bearing shapes. The lone C2J typo entry (DeleteBucketAnaxlyticsConfigurationRequest) + // is kept verbatim so the set matches C2J exactly; it simply never matches a real shape. + private static final Set EMBEDDED_ERROR_REQUESTS = Set.of( + "AbortMultipartUploadRequest", "CompleteMultipartUploadRequest", "CopyObjectRequest", + "CreateBucketRequest", "CreateMultipartUploadRequest", "CreateSessionRequest", + "DeleteBucketAnaxlyticsConfigurationRequest", "DeleteBucketCorsRequest", + "DeleteBucketEncryptionRequest", "DeleteBucketIntelligentTieringConfigurationRequest", + "DeleteBucketInventoryConfigurationRequest", "DeleteBucketLifecycleRequest", + "DeleteBucketMetricsConfigurationRequest", "DeleteBucketOwnershipControlsRequest", + "DeleteBucketPolicyRequest", "DeleteBucketReplicationRequest", "DeleteBucketRequest", + "DeleteBucketTaggingRequest", "DeleteBucketWebsiteRequest", "DeleteObjectRequest", + "DeleteObjectsRequest", "DeleteObjectTaggingRequest", "DeletePublicAccessBlockRequest", + "GetBucketAccelerateConfigurationRequest", "GetBucketAclRequest", + "GetBucketAnalyticsConfigurationRequest", "GetBucketCorsRequest", "GetBucketEncryptionRequest", + "GetBucketIntelligentTieringConfigurationRequest", "GetBucketInventoryConfigurationRequest", + "GetBucketLifecycleConfigurationRequest", "GetBucketLocationRequest", "GetBucketLoggingRequest", + "GetBucketMetricsConfigurationRequest", "GetBucketNotificationConfigurationRequest", + "GetBucketOwnershipControlsRequest", "GetBucketPolicyRequest", "GetBucketPolicyStatusRequest", + "GetBucketReplicationRequest", "GetBucketRequestPaymentRequest", "GetBucketTaggingRequest", + "GetBucketVersioningRequest", "GetBucketWebsiteRequest", "GetObjectAclRequest", + "GetObjectAttributesRequest", "GetObjectLegalHoldRequest", "GetObjectLockConfigurationRequest", + "GetObjectRetentionRequest", "GetObjectTaggingRequest", "GetPublicAccessBlockRequest", + "HeadBucketRequest", "HeadObjectRequest", "ListBucketAnalyticsConfigurationsRequest", + "ListBucketIntelligentTieringConfigurationsRequest", "ListBucketInventoryConfigurationsRequest", + "ListBucketMetricsConfigurationsRequest", "ListBucketsRequest", "ListDirectoryBucketsRequest", + "ListMultipartUploadsRequest", "ListObjectsRequest", "ListObjectsV2Request", + "ListObjectVersionsRequest", "ListPartsRequest", + "PutBucketAccelerateConfigurationRequest", "PutBucketAclRequest", + "PutBucketAnalyticsConfigurationRequest", "PutBucketCorsRequest", "PutBucketEncryptionRequest", + "PutBucketIntelligentTieringConfigurationRequest", "PutBucketInventoryConfigurationRequest", + "PutBucketLifecycleConfigurationRequest", "PutBucketLoggingRequest", + "PutBucketMetricsConfigurationRequest", "PutBucketNotificationConfigurationRequest", + "PutBucketOwnershipControlsRequest", "PutBucketPolicyRequest", "PutBucketReplicationRequest", + "PutBucketRequestPaymentRequest", "PutBucketTaggingRequest", "PutBucketVersioningRequest", + "PutBucketWebsiteRequest", "PutObjectAclRequest", "PutObjectLegalHoldRequest", + "PutObjectLockConfigurationRequest", "PutObjectRequest", "PutObjectRetentionRequest", + "PutObjectTaggingRequest", "PutPublicAccessBlockRequest", "RestoreObjectRequest", + "SelectObjectContentRequest", "UploadPartCopyRequest", "UploadPartRequest", + "WriteGetObjectResponseRequest"); + + private static Model markEmbeddedErrors(Model model) { + List marked = new ArrayList<>(); + for (StructureShape shape : model.shapes(StructureShape.class).toList()) { + if (EMBEDDED_ERROR_REQUESTS.contains(shape.getId().getName()) + && !shape.hasTrait(EmbeddedErrorsTrait.class)) { + marked.add(shape.toBuilder().addTrait(new EmbeddedErrorsTrait()).build()); + } + } + if (marked.isEmpty()) { + return model; // no request shape from the set is present (idempotent / other model). + } + return model.toBuilder().addShapes(marked.toArray(new Shape[0])).build(); } // C2J's S3RestXmlCppClientGenerator appends a `customizedAccessLogTag` map member @@ -90,6 +150,10 @@ private static Model injectAccessLogTagQuery(Model model, ServiceShape service) .addMember(MemberShape.builder() .id(req.getId().withMember("customizedAccessLogTag")) .target(mapId) + // @httpQueryParams binds this map to the query string. C2J models it as a + // querystring member on every request, which is what makes every request emit + // AddQueryStringParameters; the trait drives RequestBindings.hasQueryStringMembers. + .addTrait(new HttpQueryParamsTrait()) .build()) .build()); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java index dc04e36ef50..e0b90ce6e8a 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java @@ -235,6 +235,35 @@ void restXml_withQueryMember_emitsAddQueryStringParameters() { assertTrue(d.contains("void AddQueryStringParameters(Aws::Http::URI& uri) const override;"), d); } + private static software.amazon.smithy.model.shapes.StructureShape reqWithEmbeddedErrors() { + return software.amazon.smithy.model.shapes.StructureShape.builder() + .id("com.example#DoThingRequest") + .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms + .EmbeddedErrorsTrait()) + .build(); + } + + @Test + void restXml_withEmbeddedErrorsTrait_emitsHasEmbeddedError() { + var req = reqWithEmbeddedErrors(); var model = modelWith(req); + ProtocolTraits xml = new RestXmlProtocolTraits(); + String d = render(w -> xml.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThing(), model)); + assertTrue(d.contains("AWS_EX_API bool HasEmbeddedError(IOStream &body, " + + "const Http::HeaderValueCollection &header) const override;"), d); + String i = render(w -> xml.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertTrue(i.contains("bool DoThingRequest::HasEmbeddedError("), i); + } + + @Test + void restXml_withoutEmbeddedErrorsTrait_omitsHasEmbeddedError() { + var req = reqWith(false, false); var model = modelWith(req); + ProtocolTraits xml = new RestXmlProtocolTraits(); + String d = render(w -> xml.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThing(), model)); + assertFalse(d.contains("HasEmbeddedError"), d); + String i = render(w -> xml.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertFalse(i.contains("HasEmbeddedError"), i); + } + // ---------- Query/EC2 request contract (Axis-1 gating + protected DumpBodyToUrl) ---------- @Test diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index ecaeda4b691..c7c4902f858 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -309,6 +309,12 @@ void injectsCustomizedAccessLogTagIntoRequestAsStringMapAppendedLast() { assertEquals("smithy.api#String", mapShape.getKey().getTarget().toString()); assertEquals("smithy.api#String", mapShape.getValue().getTarget().toString()); + // Must bind to the query string (@httpQueryParams) so RequestBindings.hasQueryStringMembers + // is true and the request emits AddQueryStringParameters — matching C2J, which renders that + // method on every request via the customizedAccessLogTag querystring member. + assertTrue(tag.hasTrait(software.amazon.smithy.model.traits.HttpQueryParamsTrait.class), + "customizedAccessLogTag must carry @httpQueryParams"); + // Appended after all existing members, preserving prior order. java.util.List order = new java.util.ArrayList<>(input.getAllMembers().keySet()); assertEquals(java.util.List.of("Bucket", "Key", "customizedAccessLogTag"), order, @@ -325,6 +331,24 @@ void doesNotInjectCustomizedAccessLogTagIntoOutput() { "output shape must not gain the access-log tag member"); } + @Test + void stampsEmbeddedErrorsTraitOnRequestInC2jSet() { + ServiceShape svc = s3Service("S3"); + StructureShape inSet = StructureShape.builder().id(NS + "#CreateSessionRequest") + .addMember("Bucket", ShapeId.from("smithy.api#String")).build(); + StructureShape notInSet = StructureShape.builder().id(NS + "#SomeOtherRequest") + .addMember("Bucket", ShapeId.from("smithy.api#String")).build(); + Model m = modelWith(svc, inSet, notInSet); + Model out = S3Transforms.asTransform().apply(m, svc); + + StructureShape marked = out.expectShape(ShapeId.from(NS + "#CreateSessionRequest"), StructureShape.class); + assertTrue(marked.hasTrait(EmbeddedErrorsTrait.class), + "request in the C2J functionsWithEmbeddedErrors set must be marked"); + StructureShape unmarked = out.expectShape(ShapeId.from(NS + "#SomeOtherRequest"), StructureShape.class); + assertFalse(unmarked.hasTrait(EmbeddedErrorsTrait.class), + "request not in the set must not be marked"); + } + @Test void accessLogTagInjectionIsIdempotent() { Model m = accessLogModel(); From 1ddfb2deb1fc1410bc5537f06e106a5830b0113a Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 31 Aug 2026 15:22:26 -0400 Subject: [PATCH 30/44] Smithy: emit real (constant) HasEmbeddedError body instead of stub The XML error-sniff body is constant (not shape-dependent) across C2J's S3 request-source templates, so there is nothing to defer. Move the HasEmbeddedError helpers out of the base ProtocolTraits (they leaked XML parsing into every protocol) into RestXmlProtocolTraits, where the marker is the only emit site, and emit the real parse-body / root-is- logic. --- .../model/protocol/ProtocolTraits.java | 22 --------------- .../model/protocol/RestXmlProtocolTraits.java | 28 +++++++++++++++++++ .../model/protocol/XmlProtocolTraitsTest.java | 5 ++++ 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java index 001020ce84d..d7002148dc0 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java @@ -190,28 +190,6 @@ default void writeGetRequestSpecificHeadersImpl(CppWriter writer, String classNa }); } - /** - * Declares the {@code HasEmbeddedError} override C2J emits under - * {@code #if($shape.hasEmbeddedErrors())} in {@code RequestHeader.vm}. Text matches C2J exactly - * (unqualified {@code IOStream} / {@code Http::HeaderValueCollection}, resolved via the request - * header's {@code Aws} usings). Only S3 requests carry {@code EmbeddedErrorsTrait}, so the - * caller gates emission on that marker. - */ - default void writeHasEmbeddedErrorDecl(CppWriter writer, String exportMacro) { - writer.write("$L bool HasEmbeddedError(IOStream &body, " - + "const Http::HeaderValueCollection &header) const override;", exportMacro); - } - - /** - * Emits a stubbed {@code HasEmbeddedError} body. The real XML error-sniff is a serde concern - * deferred plugin-wide; returning {@code false} with unnamed params keeps the override present - * (byte-parity for the request header) without tripping {@code -Werror} on unused parameters. - */ - default void writeHasEmbeddedErrorImpl(CppWriter writer, String className) { - writer.write("bool $L::HasEmbeddedError(Aws::IOStream&, " - + "const Aws::Http::HeaderValueCollection&) const { return false; }", className); - } - default void writeAddQueryStringParametersImpl(CppWriter writer, String className) { writer.openBlock("void $L::AddQueryStringParameters(Aws::Http::URI& uri) const {", "}", className, () -> { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java index 54b03cf52ac..7291f863b79 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java @@ -175,4 +175,32 @@ public void writeRequestMethodImpls(CppWriter writer, String className, writeHasEmbeddedErrorImpl(writer, className); } } + + // C2J RequestHeader.vm decl: unqualified IOStream / Http::HeaderValueCollection, resolved via the + // request header's Aws usings. Only S3 requests carry EmbeddedErrorsTrait, so the caller gates. + private void writeHasEmbeddedErrorDecl(CppWriter writer, String exportMacro) { + writer.write("$L bool HasEmbeddedError(IOStream &body, " + + "const Http::HeaderValueCollection &header) const override;", exportMacro); + } + + // Constant XML error-sniff body, identical across C2J's S3 request-source templates + // (XmlRequestSource / StreamRequestSource / PutBucketNotificationConfigurationRequest): parse the + // response body as XML and report an embedded error when the root element is . It is not + // shape-dependent, so there is nothing to defer. XmlSerializer.h + UnreferencedParam.h and the + // Aws::Utils::Xml / Aws::Utils usings are already in the REQUEST_SOURCE serde includes/usings. + private void writeHasEmbeddedErrorImpl(CppWriter writer, String className) { + writer.openBlock("bool $L::HasEmbeddedError(Aws::IOStream& body, " + + "const Aws::Http::HeaderValueCollection& header) const {", "}", className, () -> { + writer.write("AWS_UNREFERENCED_PARAM(header);"); + writer.write("auto readPointer = body.tellg();"); + writer.write("Utils::Xml::XmlDocument doc = XmlDocument::CreateFromXmlStream(body);"); + writer.write("body.seekg(readPointer);"); + writer.openBlock("if (!doc.WasParseSuccessful()) {", "}", + () -> writer.write("return false;")); + writer.openBlock("if (!doc.GetRootElement().IsNull() " + + "&& doc.GetRootElement().GetName() == Aws::String(\"Error\")) {", "}", + () -> writer.write("return true;")); + writer.write("return false;"); + }); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java index e0b90ce6e8a..31336b33e23 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java @@ -251,7 +251,12 @@ void restXml_withEmbeddedErrorsTrait_emitsHasEmbeddedError() { assertTrue(d.contains("AWS_EX_API bool HasEmbeddedError(IOStream &body, " + "const Http::HeaderValueCollection &header) const override;"), d); String i = render(w -> xml.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + // The impl is the real constant XML error-sniff body (not a stub) — matches C2J's S3 + // request-source templates: parse the body and report true iff the root element is . assertTrue(i.contains("bool DoThingRequest::HasEmbeddedError("), i); + assertTrue(i.contains("XmlDocument doc = XmlDocument::CreateFromXmlStream(body);"), i); + assertTrue(i.contains("doc.GetRootElement().GetName() == Aws::String(\"Error\")"), i); + assertFalse(i.contains("return false; }"), "impl must not be the one-line stub"); } @Test From d1b2f2e1ba6a84b0c47f27b4f9b4d677588795d5 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 31 Aug 2026 16:13:10 -0400 Subject: [PATCH 31/44] Smithy: S3Transforms retypes PartNumberMarker/NextPartNumberMarker back to int --- .../model/transforms/S3Transforms.java | 31 ++++++++++++- .../model/transforms/S3TransformsTest.java | 43 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 6597d5f5924..83a7bc09fff 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -12,6 +12,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; @@ -58,7 +59,8 @@ private static Model apply(Model model, ServiceShape service) { } return markEmbeddedErrors(injectAccessLogTagQuery(normalizeReplicationStatus( expandBucketLocationConstraint(hackGetObjectResult( - addExpiresCustomization(renameCopyObjectResult(model, service), service)))), service)); + addExpiresCustomization(renameCopyObjectResult( + retypePartNumberMarkersToInteger(model), service), service)))), service)); } // C2J's S3RestXmlCppClientGenerator carries a hardcoded functionsWithEmbeddedErrors set; each @@ -331,6 +333,33 @@ private static Model addExpiresCustomization(Model model, ServiceShape service) return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } + // Both request and result on ListParts / GetObjectAttributes reference these two shapes. C2J models + // them as integers, so the shipped SDK exposes int accessors. Coral2Smithy's S3ShapeMutatorTransformer + // instead treats them as opaque pagination tokens: it leaves PartNumberMarker as Coral's string and + // retypes NextPartNumberMarker to string. Retype both back to integer here to preserve the C2J public + // API (int, not Aws::String). The paginator generator is a separate plugin that never sees this + // mutation; it keeps its own NUMERIC_TOKEN_OVERRIDES entry so its `!= 0` check matches the int result. + private static final List PART_NUMBER_MARKER_SHAPES = + List.of("PartNumberMarker", "NextPartNumberMarker"); + + private static Model retypePartNumberMarkersToInteger(Model model) { + String ns = "com.amazonaws.s3"; + List replacements = new ArrayList<>(); + for (String name : PART_NUMBER_MARKER_SHAPES) { + Shape existing = model.getShape(ShapeId.fromParts(ns, name)).orElse(null); + // absent or already integer: nothing to retype (idempotent / other model). + if (existing != null && !(existing instanceof IntegerShape)) { + IntegerShape.Builder b = IntegerShape.builder().id(existing.getId()); + existing.getAllTraits().values().forEach(b::addTrait); + replacements.add(b.build()); + } + } + if (replacements.isEmpty()) { + return model; + } + return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); + } + private static Model retypeExpiresToTimestamp(Model model, String ns) { ShapeId expiresId = ShapeId.fromParts(ns, "Expires"); Shape existing = model.getShape(expiresId).orElse(null); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index c7c4902f858..8b85e34a6b3 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -147,6 +148,48 @@ void retypesExpiresShapeToTimestamp() { assertTrue(out.expectShape(output.getMember("Expires").orElseThrow().getTarget()) instanceof TimestampShape); } + /** + * Builds an S3 model with a ListParts-style operation whose request and result reference the + * {@code PartNumberMarker} / {@code NextPartNumberMarker} shapes as strings (matching the current + * Smithy model, where Coral2Smithy treats them as opaque pagination tokens). + */ + private static Model partNumberMarkerModel() { + Shape marker = StringShape.builder().id(NS + "#PartNumberMarker").build(); + Shape nextMarker = StringShape.builder().id(NS + "#NextPartNumberMarker").build(); + StructureShape input = StructureShape.builder().id(NS + "#ListPartsRequest") + .addMember("PartNumberMarker", marker.getId()).build(); + StructureShape output = StructureShape.builder().id(NS + "#ListPartsOutput") + .addMember("PartNumberMarker", marker.getId()) + .addMember("NextPartNumberMarker", nextMarker.getId()).build(); + OperationShape op = OperationShape.builder().id(NS + "#ListParts") + .input(input.getId()).output(output.getId()).build(); + ServiceShape svc = ServiceShape.builder().id(NS + "#AmazonS3").version("2006-03-01") + .addTrait(ServiceTrait.builder().sdkId("S3").arnNamespace("s3") + .cloudFormationName("S3").cloudTrailEventSource("s3.amazonaws.com").build()) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(marker, nextMarker, input, output, op, svc).assemble().unwrap(); + } + + @Test + void retypesPartNumberMarkersToInteger() { + Model m = partNumberMarkerModel(); + assertTrue(m.expectShape(ShapeId.from(NS + "#PartNumberMarker")).isStringShape(), + "precondition: PartNumberMarker starts as a string"); + assertTrue(m.expectShape(ShapeId.from(NS + "#NextPartNumberMarker")).isStringShape(), + "precondition: NextPartNumberMarker starts as a string"); + ServiceShape svc = m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); + Model out = S3Transforms.asTransform().apply(m, svc); + assertTrue(out.expectShape(ShapeId.from(NS + "#PartNumberMarker")) instanceof IntegerShape, + "PartNumberMarker retyped to integer to preserve the shipped C2J int API"); + assertTrue(out.expectShape(ShapeId.from(NS + "#NextPartNumberMarker")) instanceof IntegerShape, + "NextPartNumberMarker retyped to integer to preserve the shipped C2J int API"); + // Request and result members both now resolve to integer targets. + StructureShape input = out.expectShape(ShapeId.from(NS + "#ListPartsRequest"), StructureShape.class); + StructureShape output = out.expectShape(ShapeId.from(NS + "#ListPartsOutput"), StructureShape.class); + assertTrue(out.expectShape(input.getMember("PartNumberMarker").orElseThrow().getTarget()) instanceof IntegerShape); + assertTrue(out.expectShape(output.getMember("NextPartNumberMarker").orElseThrow().getTarget()) instanceof IntegerShape); + } + @Test void addsExpiresStringToOutputAndDeprecatesExpires() { Model m = expiresModel(); From 47eeef6d15e763c32ba795bf689c9a38a36b3a0a Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 31 Aug 2026 16:43:06 -0400 Subject: [PATCH 32/44] Smithy: S3Transforms ports IsStreaming override and checksum-member setter customizations --- .../generators/model/MemberRenderer.java | 12 +++ .../model/renderers/RequestRenderer.java | 7 ++ .../model/transforms/ChecksumMemberTrait.java | 27 +++++++ .../transforms/OverrideStreamingTrait.java | 27 +++++++ .../model/transforms/S3Transforms.java | 74 ++++++++++++++++++- .../model/MemberRendererOutputTest.java | 40 ++++++++++ .../generators/model/RequestRendererTest.java | 50 +++++++++++++ .../model/transforms/S3TransformsTest.java | 73 ++++++++++++++++++ 8 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java index fb130f6ae65..72bdf30ea40 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java @@ -7,6 +7,7 @@ import static com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper.isPrimitive; import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ChecksumMemberTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ListShape; @@ -124,6 +125,9 @@ public void renderPublicAccessors(CppWriter writer) { writer.write("return *this;"); }); } else { + // S3 checksum members (stamped by S3Transforms) also select the ChecksumAlgorithm enum + // in their setter, matching C2J's ModelClassMembersAndInlines.vm isChecksumMember path. + java.util.Optional checksum = member.getTrait(ChecksumMemberTrait.class); writer.write("template ", templateParam, cppType); writer.openBlock("void Set$L($L&& value) {", "}", methodName, templateParam, () -> { writer.write("$LHasBeenSet = true;", fieldName); @@ -136,7 +140,15 @@ public void renderPublicAccessors(CppWriter writer) { } else { writer.write("$L = std::forward<$L>(value);", fieldName, templateParam); } + checksum.ifPresent(t -> + writer.write("SetChecksumAlgorithm(ChecksumAlgorithm::$L);", t.getValue())); }); + checksum.ifPresent(t -> + writer.openBlock("inline void Set$L(const char* value) {", "}", methodName, () -> { + writer.write("$LHasBeenSet = true;", fieldName); + writer.write("$L.assign(value);", fieldName); + writer.write("SetChecksumAlgorithm(ChecksumAlgorithm::$L);", t.getValue()); + })); writer.write("template ", templateParam, cppType); writer.openBlock("$L& With$L($L&& value) {", "}", className, methodName, templateParam, () -> { writer.write("Set$L(std::forward<$L>(value));", methodName, templateParam); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index 8622fafc506..1189ffffa7e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -14,6 +14,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.RequestInfo; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.OverrideStreamingTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.Emit; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.SmithyEndpointsJmesPathVisitor; import software.amazon.smithy.jmespath.JmespathExpression; @@ -155,6 +156,12 @@ private void renderHeader(CppWriterDelegator writerDelegator, renderContentMd5Decl(writer, operation); renderRequestCompressionDecl(writer, operation); + // S3 flips a couple of streaming-base requests back to non-streaming (C2J + // isOverrideStreaming); the marker is stamped by S3Transforms. + if (shape.hasTrait(OverrideStreamingTrait.class)) { + writer.write("$L bool IsStreaming() const override { return false; }", ctx.exportMacro()); + } + if (streamingResponse) { String handlerType = operation.getId().getName() + "Handler"; writer.write(""); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java new file mode 100644 index 00000000000..a0bd440e336 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java @@ -0,0 +1,27 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.StringTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link S3Transforms} on each S3 request + * member that C2J's {@code S3RestXmlCppClientGenerator} flags via {@code member.setChecksumMember(true)} + * + {@code member.setChecksumEnumMember(...)} — the {@code ChecksumCRC32}/{@code ChecksumSHA256}/etc. + * members of any request that also carries a {@code ChecksumAlgorithm} member. The stored value is the + * matching {@code ChecksumAlgorithm} enum constant (e.g. {@code CRC32}). Member rendering turns the + * marker into the C2J {@code ModelClassMembersAndInlines.vm} behavior: each setter also calls + * {@code SetChecksumAlgorithm(ChecksumAlgorithm::)}, plus a {@code const char*} overload that does + * the same. Kept as a marker + generic renderer rule so the member renderer stays service-agnostic. + */ +public final class ChecksumMemberTrait extends StringTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#checksumMember"); + + public ChecksumMemberTrait(String algorithmEnum) { + super(ID, algorithmEnum, SourceLocation.NONE); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java new file mode 100644 index 00000000000..dee0eebb12b --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java @@ -0,0 +1,27 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link S3Transforms} on each S3 + * request structure that C2J's {@code S3RestXmlCppClientGenerator} lists in its + * {@code REQUESTS_TO_OVERRIDE_STREAMING} set ({@code shape.setOverrideStreaming(true)}). These + * requests derive from {@code StreamingS3Request} (a typedef for {@code AmazonStreamingWebServiceRequest}, + * whose {@code IsStreaming()} returns {@code true}) yet must report non-streaming, so request rendering + * turns the marker into the {@code bool IsStreaming() const override { return false; }} override that + * {@code RequestHeader.vm} emits under {@code #if($shape.isOverrideStreaming())}. Kept as a marker + + * generic renderer rule (not a service-name {@code if}) so the renderer stays service-agnostic. + */ +public final class OverrideStreamingTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#overrideStreaming"); + + public OverrideStreamingTrait() { + super(ID, Node.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 83a7bc09fff..152635b1050 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -57,10 +57,82 @@ private static Model apply(Model model, ServiceShape service) { if (!"s3".equals(name) && !"s3-crt".equals(name)) { return model; } - return markEmbeddedErrors(injectAccessLogTagQuery(normalizeReplicationStatus( + Model result = markEmbeddedErrors(injectAccessLogTagQuery(normalizeReplicationStatus( expandBucketLocationConstraint(hackGetObjectResult( addExpiresCustomization(renameCopyObjectResult( retypePartNumberMarkersToInteger(model), service), service)))), service)); + result = markOverrideStreaming(result); + return markChecksumMembers(result, service); + } + + // C2J's S3RestXmlCppClientGenerator flips these two requests' isOverrideStreaming on. Both derive + // from StreamingS3Request (== AmazonStreamingWebServiceRequest, whose IsStreaming() returns true), + // so they must override IsStreaming() back to false; RequestRenderer emits that for marked shapes. + private static final Set REQUESTS_TO_OVERRIDE_STREAMING = Set.of( + "PutBucketPolicyRequest", "PutObjectAnnotationRequest"); + + private static Model markOverrideStreaming(Model model) { + List marked = new ArrayList<>(); + for (StructureShape shape : model.shapes(StructureShape.class).toList()) { + if (REQUESTS_TO_OVERRIDE_STREAMING.contains(shape.getId().getName()) + && !shape.hasTrait(OverrideStreamingTrait.class)) { + marked.add(shape.toBuilder().addTrait(new OverrideStreamingTrait()).build()); + } + } + if (marked.isEmpty()) { + return model; // neither request is present (idempotent / other model). + } + return model.toBuilder().addShapes(marked.toArray(new Shape[0])).build(); + } + + // C2J's S3RestXmlCppClientGenerator maps each checksum member shape name to its ChecksumAlgorithm + // enum constant; every request that also carries a ChecksumAlgorithm member gets these members + // flagged so their setters also call SetChecksumAlgorithm(...). ChecksumCRC64NVME is intentionally + // absent (C2J never listed it), so it keeps a plain setter. + private static final Map CHECKSUM_MEMBERS_ENUMS = Map.ofEntries( + Map.entry("ChecksumCRC32", "CRC32"), + Map.entry("ChecksumCRC32C", "CRC32C"), + Map.entry("ChecksumSHA1", "SHA1"), + Map.entry("ChecksumSHA256", "SHA256"), + Map.entry("ChecksumSHA512", "SHA512"), + Map.entry("ChecksumXXHASH64", "XXHASH64"), + Map.entry("ChecksumXXHASH3", "XXHASH3"), + Map.entry("ChecksumXXHASH128", "XXHASH128"), + Map.entry("ChecksumMD5", "MD5")); + + private static Model markChecksumMembers(Model model, ServiceShape service) { + Set inputShapes = TopDownIndex.of(model).getContainedOperations(service).stream() + .map(OperationShape::getInputShape) + .filter(id -> !id.equals(UnitTypeTrait.UNIT)) + .collect(Collectors.toSet()); + List replacements = new ArrayList<>(); + for (StructureShape req : model.shapes(StructureShape.class).toList()) { + // Only request shapes that already carry a ChecksumAlgorithm member (so SetChecksumAlgorithm + // exists), and only when they hold at least one not-yet-marked checksum member. + boolean isChecksumRequest = inputShapes.contains(req.getId()) + && req.getMember("ChecksumAlgorithm").isPresent(); + boolean needsStamp = isChecksumRequest && req.getAllMembers().values().stream().anyMatch(m -> + CHECKSUM_MEMBERS_ENUMS.containsKey(m.getTarget().getName()) + && !m.hasTrait(ChecksumMemberTrait.class)); + if (needsStamp) { + StructureShape.Builder b = StructureShape.builder().id(req.getId()); + req.getAllTraits().values().forEach(b::addTrait); + for (MemberShape m : req.getAllMembers().values()) { + String enumValue = CHECKSUM_MEMBERS_ENUMS.get(m.getTarget().getName()); + b.addMember(m.getMemberName(), m.getTarget(), mb -> { + m.getAllTraits().values().forEach(mb::addTrait); + if (enumValue != null && !m.hasTrait(ChecksumMemberTrait.class)) { + mb.addTrait(new ChecksumMemberTrait(enumValue)); + } + }); + } + replacements.add(b.build()); + } + } + if (replacements.isEmpty()) { + return model; // no qualifying request (idempotent / other model). + } + return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } // C2J's S3RestXmlCppClientGenerator carries a hardcoded functionsWithEmbeddedErrors set; each diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java index 8eafddf2c4d..7925f00304e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java @@ -5,6 +5,7 @@ package com.amazonaws.util.awsclientsmithygenerator.generators.model; import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ChecksumMemberTrait; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.*; @@ -73,6 +74,45 @@ void fullOutput_matchesExpectedPattern() { assertTrue(privOutput.contains("bool m_hashKeyRangeHasBeenSet = false;")); } + @Test + void checksumMember_setterAlsoSelectsAlgorithm_andEmitsConstCharOverload() { + // C2J ModelClassMembersAndInlines.vm isChecksumMember path: each checksum setter also calls + // SetChecksumAlgorithm(ChecksumAlgorithm::), and a const char* overload does the same. + StringShape str = StringShape.builder().id("com.example#ChecksumCRC32").build(); + StructureShape shape = StructureShape.builder() + .id("com.example#PutObjectRequest") + .addMember("ChecksumCRC32", str.getId(), b -> b.addTrait(new ChecksumMemberTrait("CRC32"))) + .build(); + Model model = Model.builder().addShapes(str, shape).build(); + + CppWriter w = new CppWriter(); + MemberRenderer.forStructure(model, shape, "PutObjectRequest").renderPublicAccessors(w); + String out = w.toString(); + + assertTrue(out.contains("void SetChecksumCRC32(ChecksumCRC32T&& value)"), out); + assertTrue(out.contains("m_checksumCRC32 = std::forward(value);"), out); + assertTrue(out.contains("SetChecksumAlgorithm(ChecksumAlgorithm::CRC32);"), out); + assertTrue(out.contains("inline void SetChecksumCRC32(const char* value) {"), out); + assertTrue(out.contains("m_checksumCRC32.assign(value);"), out); + } + + @Test + void nonChecksumStringMember_hasNoAlgorithmSideEffectOrConstCharOverload() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape shape = StructureShape.builder() + .id("com.example#PutObjectRequest") + .addMember("Key", str.getId()) + .build(); + Model model = Model.builder().addShapes(str, shape).build(); + + CppWriter w = new CppWriter(); + MemberRenderer.forStructure(model, shape, "PutObjectRequest").renderPublicAccessors(w); + String out = w.toString(); + + assertFalse(out.contains("SetChecksumAlgorithm"), out); + assertFalse(out.contains("const char* value"), out); + } + @Test void sparseListAndMap_emitOptionalTypesAndAddOverloads() { // Mirrors C2J's generated SparseNullsOperationRequest.h: a @sparse list/map wraps its diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index aa52eca9798..23127963a4e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -8,6 +8,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.OverrideStreamingTrait; import org.junit.jupiter.api.Test; import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.model.Model; @@ -92,6 +93,55 @@ private static String renderStreamingOp(boolean inputStreams, boolean outputStre .orElseThrow(); } + private static Model overrideStreamingModel(boolean marked) { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape.Builder inB = StructureShape.builder() + .id("com.example#DoThingRequest").addMember("name", str.getId()); + if (marked) { + inB.addTrait(new OverrideStreamingTrait()); + } + StructureShape input = inB.build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); + OperationShape op = OperationShape.builder().id("com.example#DoThing") + .input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder().id("com.example#Example") + .version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, input, output, op, service).build(); + } + + private static String renderDoThingRequestHeader(Model model) { + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + Protocol protocol = ProtocolResolver.resolve(service, model); + RequestRenderer renderer = new RequestRenderer( + ShapeClassifier.classify(model, service, protocol).requests(), + new RenderContext(model, service, ProtocolResolver.traitsFor(protocol), + "Example", "AWS_EXAMPLE_API", "example")); + renderer.render(delegator); + delegator.flushWriters(); + return manifest.getFileString( + manifest.getFiles().stream() + .filter(p -> p.toString().endsWith("DoThingRequest.h")) + .findFirst().orElseThrow()) + .orElseThrow(); + } + + @Test + void overrideStreamingTrait_emitsIsStreamingFalse() { + String h = renderDoThingRequestHeader(overrideStreamingModel(true)); + assertTrue(h.contains("bool IsStreaming() const override { return false; }"), + "OverrideStreamingTrait must emit the non-streaming override: " + h); + } + + @Test + void withoutOverrideStreamingTrait_omitsIsStreaming() { + String h = renderDoThingRequestHeader(overrideStreamingModel(false)); + assertFalse(h.contains("IsStreaming"), + "unmarked requests must not emit IsStreaming: " + h); + } + @Test void streamingResponseRequest_hasEventStreamAugmentation() { // Model: operation with streaming OUTPUT only (like SubscribeToShard / ConverseStream) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 8b85e34a6b3..827178f250a 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -190,6 +190,79 @@ void retypesPartNumberMarkersToInteger() { assertTrue(out.expectShape(output.getMember("NextPartNumberMarker").orElseThrow().getTarget()) instanceof IntegerShape); } + @Test + void marksOverrideStreamingRequests() { + StructureShape put = StructureShape.builder().id(NS + "#PutObjectAnnotationRequest").build(); + StructureShape policy = StructureShape.builder().id(NS + "#PutBucketPolicyRequest").build(); + StructureShape other = StructureShape.builder().id(NS + "#GetObjectRequest").build(); + ServiceShape svc = s3Service("S3"); + Model out = S3Transforms.asTransform().apply(modelWith(svc, put, policy, other), svc); + assertTrue(out.expectShape(put.getId()).hasTrait(OverrideStreamingTrait.class), + "PutObjectAnnotationRequest is in REQUESTS_TO_OVERRIDE_STREAMING"); + assertTrue(out.expectShape(policy.getId()).hasTrait(OverrideStreamingTrait.class), + "PutBucketPolicyRequest is in REQUESTS_TO_OVERRIDE_STREAMING"); + assertFalse(out.expectShape(other.getId()).hasTrait(OverrideStreamingTrait.class), + "other requests are untouched"); + } + + /** + * Builds an S3 model with a PutObject-style request carrying the checksum members plus a + * non-checksum member; {@code withAlgorithmMember} controls whether the request also has the + * {@code ChecksumAlgorithm} member that gates the C2J customization. + */ + private static Model checksumModel(boolean withAlgorithmMember) { + Shape crc32 = StringShape.builder().id(NS + "#ChecksumCRC32").build(); + Shape sha256 = StringShape.builder().id(NS + "#ChecksumSHA256").build(); + Shape crc64 = StringShape.builder().id(NS + "#ChecksumCRC64NVME").build(); + Shape algo = StringShape.builder().id(NS + "#ChecksumAlgorithm").build(); + Shape key = StringShape.builder().id(NS + "#ObjectKey").build(); + StructureShape.Builder reqB = StructureShape.builder().id(NS + "#PutObjectRequest") + .addMember("ChecksumCRC32", crc32.getId()) + .addMember("ChecksumSHA256", sha256.getId()) + .addMember("ChecksumCRC64NVME", crc64.getId()) + .addMember("Key", key.getId()); + if (withAlgorithmMember) { + reqB.addMember("ChecksumAlgorithm", algo.getId()); + } + StructureShape req = reqB.build(); + StructureShape output = StructureShape.builder().id(NS + "#PutObjectOutput").build(); + OperationShape op = OperationShape.builder().id(NS + "#PutObject") + .input(req.getId()).output(output.getId()).build(); + ServiceShape svc = ServiceShape.builder().id(NS + "#AmazonS3").version("2006-03-01") + .addTrait(ServiceTrait.builder().sdkId("S3").arnNamespace("s3") + .cloudFormationName("S3").cloudTrailEventSource("s3.amazonaws.com").build()) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(crc32, sha256, crc64, algo, key, req, output, op, svc) + .assemble().unwrap(); + } + + @Test + void marksChecksumMembersOnRequestWithChecksumAlgorithm() { + Model m = checksumModel(true); + ServiceShape svc = m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); + Model out = S3Transforms.asTransform().apply(m, svc); + StructureShape req = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); + assertEquals("CRC32", + req.getMember("ChecksumCRC32").orElseThrow().expectTrait(ChecksumMemberTrait.class).getValue()); + assertEquals("SHA256", + req.getMember("ChecksumSHA256").orElseThrow().expectTrait(ChecksumMemberTrait.class).getValue()); + // ChecksumCRC64NVME is intentionally absent from C2J's map — it keeps a plain setter. + assertFalse(req.getMember("ChecksumCRC64NVME").orElseThrow().hasTrait(ChecksumMemberTrait.class), + "CRC64NVME is not a C2J checksum member"); + assertFalse(req.getMember("Key").orElseThrow().hasTrait(ChecksumMemberTrait.class), + "non-checksum members are untouched"); + } + + @Test + void doesNotMarkChecksumMembersWithoutChecksumAlgorithm() { + Model m = checksumModel(false); + ServiceShape svc = m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); + Model out = S3Transforms.asTransform().apply(m, svc); + StructureShape req = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); + assertFalse(req.getMember("ChecksumCRC32").orElseThrow().hasTrait(ChecksumMemberTrait.class), + "no ChecksumAlgorithm member => C2J does not flag the checksum members"); + } + @Test void addsExpiresStringToOutputAndDeprecatesExpires() { Model m = expiresModel(); From 1affeec5b9bb43acc3daa6b121d453f315ff20d5 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 1 Sep 2026 11:51:21 -0400 Subject: [PATCH 33/44] Query string parameters and Request specific headers parity Smithy: drop dead CBOR HTTP-binding wiring and correct REQUEST_SOURCE includes Smithy: share RequestHeaderSerializer value expression across scalar and list paths Smithy: RPC protocols (awsJson, rpcv2-cbor) do not wire-serialize HTTP-binding members Smithy: query enums gate on HasBeenSet only (no NOT_SET), matching C2J Smithy: lowercase request header location names to match C2J Smithy: S3 customizedAccessLogTag x- query filter Smithy: dedupe RequestQuerySerializer timestamp stream expression Smithy: RequestQuerySerializer list and query-params-map serialization Smithy: RequestHeaderSerializer prefix-header map and list serialization Smithy: RequestHeaderSerializer scalar header-member serialization --- .../model/protocol/CborProtocolTraits.java | 21 +- .../model/protocol/JsonProtocolTraits.java | 31 +- .../model/protocol/ProtocolTraits.java | 38 ++- .../protocol/QueryXmlProtocolTraits.java | 15 +- .../model/protocol/RestXmlProtocolTraits.java | 24 +- .../renderers/RequestHeaderSerializer.java | 169 ++++++++++ .../renderers/RequestQuerySerializer.java | 187 +++++++++++ .../CustomizedAccessLogTagTrait.java | 28 ++ .../model/transforms/S3Transforms.java | 10 +- .../ProtocolTraitsCharacterizationTest.java | 11 +- .../protocol/CborProtocolTraitsTest.java | 21 ++ .../protocol/JsonProtocolTraitsTest.java | 34 +- .../RequestHeaderSerializerTest.java | 317 ++++++++++++++++++ .../renderers/RequestQuerySerializerTest.java | 268 +++++++++++++++ .../model/transforms/S3TransformsTest.java | 17 + 15 files changed, 1164 insertions(+), 27 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializerTest.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializerTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java index 8baef186b52..1a292aa8706 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java @@ -37,6 +37,12 @@ public boolean widensIntegers() { return true; } + @Override + public boolean serializesHttpBindingMembers() { + // rpcv2Cbor is an RPC protocol: @httpHeader / @httpQuery members go into the body, not the wire. + return false; + } + @Override public void writeShapeForwardDeclarations(CppWriter writer) { writer.writeNamespaceOpen("Utils"); @@ -86,9 +92,11 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of("aws/crt/cbor/Cbor.h"); - // All source kinds share one union (supersets allowed). Usings are unchanged. - case SUBOBJECT_SOURCE: + // All source kinds share one include set. RPC CBOR is an RPC protocol: request sources + // never run the shared @httpQuery / @httpHeader serializers, so they need no URI / + // StringUtils includes — REQUEST_SOURCE carries the same set as every other source kind. case REQUEST_SOURCE: + case SUBOBJECT_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: case EVENT_HANDLER_SOURCE: @@ -166,7 +174,8 @@ public void writeRequestMethodDecls(CppWriter writer, String exportMacro, // (content-type / smithy-protocol / accept) regardless of member bindings. writer.write(""); writeGetRequestSpecificHeadersDecl(writer, exportMacro); - if (RequestBindings.hasQueryStringMembers(shape, model)) { + // RPC CBOR routes @httpQuery members to the body, so no AddQueryStringParameters is emitted. + if (serializesHttpBindingMembers() && RequestBindings.hasQueryStringMembers(shape, model)) { writer.write(""); writeAddQueryStringParametersDecl(writer, exportMacro); } @@ -204,11 +213,13 @@ public void writeRequestMethodImpls(CppWriter writer, String className, } writer.write("headers.emplace(Aws::Http::SMITHY_PROTOCOL_HEADER, Aws::RPC_V2_CBOR);"); writer.write("headers.emplace(Aws::Http::ACCEPT_HEADER, Aws::CBOR_CONTENT_TYPE);"); + // RPC CBOR routes @httpHeader members to the body, so no member header serialization + // follows the fixed protocol headers. writer.write("return headers;"); }); - if (RequestBindings.hasQueryStringMembers(shape, model)) { + if (serializesHttpBindingMembers() && RequestBindings.hasQueryStringMembers(shape, model)) { writer.write(""); - writeAddQueryStringParametersImpl(writer, className); + writeAddQueryStringParametersImpl(writer, className, shape, model); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java index 9996841ba71..eace0615ab8 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java @@ -88,10 +88,21 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of(); + // Request sources additionally serialize @httpQuery members via the shared query + // serializer, which needs URI (AddQueryStringParameter) and StringUtils. These are + // added only here to avoid widening the other source kinds. + case REQUEST_SOURCE: + return List.of( + "aws/core/utils/json/JsonSerializer.h", + "aws/core/utils/UnreferencedParam.h", + "aws/core/utils/memory/stl/AWSStringStream.h", + "aws/core/utils/HashingUtils.h", + "aws/core/utils/StringUtils.h", + "aws/core/http/URI.h", + "utility"); // All source kinds share one union (supersets allowed: a .cpp may carry an // include it doesn't strictly use). Usings are unchanged; only #includes widen. case SUBOBJECT_SOURCE: - case REQUEST_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: case EVENT_HANDLER_SOURCE: @@ -157,6 +168,12 @@ public boolean hasTargetHeader() { return protocol == Protocol.JSON; } + @Override + public boolean serializesHttpBindingMembers() { + // rest-json honors HTTP bindings; awsJson (RPC) routes those members to the body. + return protocol == Protocol.REST_JSON; + } + @Override public void writeRequestMethodDecls(CppWriter writer, String exportMacro, StructureShape shape, OperationShape operation, Model model) { @@ -165,11 +182,12 @@ public void writeRequestMethodDecls(CppWriter writer, String exportMacro, if (RequestBindings.emitsSerializePayload(operation, model)) { writer.write("$L Aws::String SerializePayload() const override;", exportMacro); } - if (hasTargetHeader() || RequestBindings.hasHeaderMembers(shape, model)) { + if (hasTargetHeader() + || (serializesHttpBindingMembers() && RequestBindings.hasHeaderMembers(shape, model))) { writer.write(""); writeGetRequestSpecificHeadersDecl(writer, exportMacro); } - if (RequestBindings.hasQueryStringMembers(shape, model)) { + if (serializesHttpBindingMembers() && RequestBindings.hasQueryStringMembers(shape, model)) { writer.write(""); writeAddQueryStringParametersDecl(writer, exportMacro); } @@ -183,13 +201,14 @@ public void writeRequestMethodImpls(CppWriter writer, String className, String payloadBody = protocol == Protocol.JSON ? "\"{}\"" : "{}"; writer.write("Aws::String $L::SerializePayload() const { return $L; }", className, payloadBody); } - if (hasTargetHeader() || RequestBindings.hasHeaderMembers(shape, model)) { + if (hasTargetHeader() + || (serializesHttpBindingMembers() && RequestBindings.hasHeaderMembers(shape, model))) { writer.write(""); writeGetRequestSpecificHeadersImpl(writer, className, shape, operation, service, model); } - if (RequestBindings.hasQueryStringMembers(shape, model)) { + if (serializesHttpBindingMembers() && RequestBindings.hasQueryStringMembers(shape, model)) { writer.write(""); - writeAddQueryStringParametersImpl(writer, className); + writeAddQueryStringParametersImpl(writer, className, shape, model); } } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java index d7002148dc0..4954d7fe277 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java @@ -7,6 +7,8 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppNames; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestHeaderSerializer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestQuerySerializer; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; @@ -147,6 +149,23 @@ default boolean hasTargetHeader() { return false; } + /** + * Whether this protocol honors HTTP binding traits ({@code @httpHeader} / + * {@code @httpPrefixHeaders} / {@code @httpQuery} / {@code @httpQueryParams}) by serializing + * those members onto the wire (request headers / query string). + * + *

REST protocols (rest-json, rest-xml, query/ec2) return {@code true}. RPC protocols + * (awsJson1_0/1_1, rpcv2Cbor) route these members into the request body instead, so + * they return {@code false}: their {@code GetRequestSpecificHeaders} still emits the fixed + * protocol headers ({@code X-Amz-Target} for awsJson; {@code Content-Type}/{@code smithy-protocol}/ + * {@code Accept} for CBOR), but no member header/query serialization is emitted, and no + * {@code AddQueryStringParameters} method is generated. Matches the legacy C2J per-protocol + * behavior (byte-parity reference). + */ + default boolean serializesHttpBindingMembers() { + return true; + } + /** * Whether {@code integer} members widen to {@code int64_t} (rather than {@code int}) in this * protocol's sub-object and result headers. C2J does this only for CBOR @@ -186,15 +205,28 @@ default void writeGetRequestSpecificHeadersImpl(CppWriter writer, String classNa writer.write("headers.insert(Aws::Http::HeaderValuePair(\"X-Amz-Target\", \"$L.$L\"));", service.getId().getName(), operation.getId().getName()); } + // C2J declares the stringstream once, immediately after `headers`, whenever the request + // has ≥1 header member (even all-enum/timestamp members, which never use it). RPC + // protocols route HTTP-binding members to the body, so neither the stringstream nor the + // member serialization is emitted for them. + if (serializesHttpBindingMembers() && RequestBindings.hasHeaderMembers(shape, model)) { + writer.write("Aws::StringStream ss;"); + } + if (serializesHttpBindingMembers()) { + RequestHeaderSerializer.render(writer, shape, model); + } writer.write("return headers;"); }); } - default void writeAddQueryStringParametersImpl(CppWriter writer, String className) { + default void writeAddQueryStringParametersImpl(CppWriter writer, String className, + StructureShape shape, Model model) { writer.openBlock("void $L::AddQueryStringParameters(Aws::Http::URI& uri) const {", "}", className, () -> { - writer.write("AWS_UNREFERENCED_PARAM(uri);"); - writer.write("// TODO: serialize httpQuery/httpQueryParams members"); + // C2J declares the stringstream unconditionally in AddQueryStringParameters; every query + // case routes its value through it. + writer.write("Aws::StringStream ss;"); + RequestQuerySerializer.render(writer, shape, model); }); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java index b4409b578ca..4fdd005e202 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java @@ -95,8 +95,19 @@ public List serdeIncludes(FileKind kind) { case RESULT_HEADER: // Query/EC2 result headers forward-declare XmlDocument; no serde include. return List.of(); - case SUBOBJECT_SOURCE: + // Request sources additionally serialize @httpQuery members via the shared query + // serializer, which needs URI (AddQueryStringParameter), StringUtils, and the + // stringstream. URI.h is added only here to avoid widening the other source kinds. case REQUEST_SOURCE: + return List.of( + "aws/core/utils/xml/XmlSerializer.h", + "aws/core/utils/logging/LogMacros.h", + "aws/core/utils/UnreferencedParam.h", + "aws/core/utils/StringUtils.h", + "aws/core/utils/memory/stl/AWSStringStream.h", + "aws/core/utils/HashingUtils.h", + "aws/core/http/URI.h"); + case SUBOBJECT_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: case EVENT_HANDLER_SOURCE: @@ -230,7 +241,7 @@ public void writeRequestMethodImpls(CppWriter writer, String className, } if (RequestBindings.hasQueryStringMembers(shape, model)) { writer.write(""); - writeAddQueryStringParametersImpl(writer, className); + writeAddQueryStringParametersImpl(writer, className, shape, model); } writer.write(""); writer.write("void $L::DumpBodyToUrl(Aws::Http::URI& uri) const { uri.SetQueryString(SerializePayload()); }", diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java index 7291f863b79..db5838d18e9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java @@ -75,9 +75,22 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of(); - // All source kinds share one union (supersets allowed). Usings are unchanged. - case SUBOBJECT_SOURCE: + // Request sources additionally serialize @httpHeader/@httpQuery members, which need + // StringUtils (to_string), URI (URLEncodePath for x-amz-copy-source), and + // (std::accumulate for comma-joined list headers). C2J pulls these per-shape; the + // data-driven set carries them for every request source (superset). case REQUEST_SOURCE: + return List.of( + "aws/core/utils/xml/XmlSerializer.h", + "aws/core/utils/memory/stl/AWSStringStream.h", + "aws/core/utils/UnreferencedParam.h", + "aws/core/utils/HashingUtils.h", + "aws/core/utils/StringUtils.h", + "aws/core/http/URI.h", + "numeric", + "utility"); + // The remaining source kinds share one union (supersets allowed). + case SUBOBJECT_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: case EVENT_HANDLER_SOURCE: @@ -99,9 +112,12 @@ public List serdeUsings(FileKind kind) { switch (kind) { case EVENT_HANDLER_SOURCE: return List.of(serdeNamespace()); + // Request sources add Aws::Http so URI::URLEncodePath resolves unqualified (C2J emits + // `using namespace Aws::Http;` in every XML request source that serializes headers/query). + case REQUEST_SOURCE: + return List.of("Aws::Utils::Xml", "Aws::Utils", "Aws::Http"); case RESULT_SOURCE: case INITIAL_RESPONSE_SOURCE: - case REQUEST_SOURCE: case SUBOBJECT_SOURCE: return List.of("Aws::Utils::Xml", "Aws::Utils"); default: @@ -168,7 +184,7 @@ public void writeRequestMethodImpls(CppWriter writer, String className, } if (RequestBindings.hasQueryStringMembers(shape, model)) { writer.write(""); - writeAddQueryStringParametersImpl(writer, className); + writeAddQueryStringParametersImpl(writer, className, shape, model); } if (shape.hasTrait(EmbeddedErrorsTrait.class)) { writer.write(""); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java new file mode 100644 index 00000000000..82cd84eed55 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java @@ -0,0 +1,169 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppNames; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.HttpHeaderTrait; +import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; +import software.amazon.smithy.model.traits.SparseTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; + +import java.util.Locale; + +/** + * Emits the {@code @httpHeader} member-serialization loop body for a request (operation-input) + * structure's {@code GetRequestSpecificHeaders()}, byte-matching the legacy C2J + * {@code ModelClassHeaderMembersSource.vm}. + * + *

Protocol-agnostic: the member serialization is byte-identical across REST-XML, JSON, + * REST-JSON, Query-XML, EC2, and CBOR, so this renderer never branches on protocol. The + * caller ({@code ProtocolTraits.writeGetRequestSpecificHeadersImpl}) owns the surrounding + * {@code Aws::Http::HeaderValueCollection headers;} / {@code Aws::StringStream ss;} declarations, + * the protocol prologue (e.g. {@code X-Amz-Target}), and {@code return headers;}. + * + *

Every member is {@code HasBeenSet}-gated (C2J clears {@code required} on all members), and + * enum members additionally guard against {@code ::NOT_SET}. + * + *

Scope: {@code @httpHeader} members (string / {@code x-amz-copy-source} / enum / boolean / + * blob / timestamp scalars, plus lists joined via {@code std::accumulate}) and + * {@code @httpPrefixHeaders} maps (looped, with sparse-value {@code has_value()} unwrapping). + */ +public final class RequestHeaderSerializer { + + private RequestHeaderSerializer() {} + + /** + * Emits the header-member serialization for every header-bound member of {@code shape}, in + * model order: {@code @httpHeader} scalars/lists, and {@code @httpPrefixHeaders} maps. Members + * carrying neither trait are skipped. + */ + public static void render(CppWriter writer, StructureShape shape, Model model) { + for (MemberShape member : shape.getAllMembers().values()) { + // C2J lowercases HTTP header location names (and @httpPrefixHeaders prefixes); query + // names stay case-sensitive. Locale.ROOT avoids locale-dependent casing surprises. + member.getTrait(HttpHeaderTrait.class).ifPresent(trait -> + renderHeaderMember(writer, member, trait.getValue().toLowerCase(Locale.ROOT), model)); + member.getTrait(HttpPrefixHeadersTrait.class).ifPresent(trait -> + renderPrefixHeadersMap(writer, member, trait.getValue().toLowerCase(Locale.ROOT), model)); + } + } + + private static void renderHeaderMember(CppWriter writer, MemberShape member, String location, + Model model) { + Shape target = model.expectShape(member.getTarget()); + String field = CppNames.fieldName(member.getMemberName()); + + if (target.isListShape()) { + Shape element = model.expectShape(target.asListShape().get().getMember().getTarget()); + writer.openBlock("if ($LHasBeenSet) {", "}", field, () -> + renderListBody(writer, field, location, element, model)); + return; + } + + if (CppTypeMapper.isEnum(target)) { + String enumType = CppTypeMapper.getCppType(target, model, false); + writer.openBlock("if ($1LHasBeenSet && $1L != $2L::NOT_SET) {", "}", field, enumType, () -> + writer.write("headers.emplace(\"$1L\", $2L);", + location, headerValueExpression(target, field, model))); + return; + } + + writer.openBlock("if ($LHasBeenSet) {", "}", field, () -> + renderScalarBody(writer, field, location, target, model)); + } + + // @httpPrefixHeaders map: each entry becomes a header whose name is the trait prefix + // concatenated with the entry key. A @sparse map's value is Aws::Crt::Optional, so the emplace + // is guarded on has_value() and unwrapped via value(). + private static void renderPrefixHeadersMap(CppWriter writer, MemberShape member, String prefix, + Model model) { + Shape target = model.expectShape(member.getTarget()); + String field = CppNames.fieldName(member.getMemberName()); + boolean sparse = target.hasTrait(SparseTrait.class); + writer.openBlock("if ($LHasBeenSet) {", "}", field, () -> + writer.openBlock("for (const auto& item : $L) {", "}", field, () -> { + writer.write("ss << \"$L\" << item.first;", prefix); + if (sparse) { + writer.write( + "if (item.second.has_value()) { headers.emplace(ss.str(), item.second.value()); }"); + } else { + writer.write("headers.emplace(ss.str(), item.second);"); + } + writer.write("ss.str(\"\");"); + })); + } + + // @httpHeader list: comma-joins the elements into a single header value via std::accumulate. + private static void renderListBody(CppWriter writer, String field, String location, Shape element, + Model model) { + String elementType = CppTypeMapper.getCppType(element, model, false); + writer.write("headers.emplace(\"$1L\", std::accumulate(std::begin($2L), std::end($2L), Aws::String{},", + location, field); + writer.write(" [](const Aws::String& acc, const $L& item) -> Aws::String {", elementType); + writer.write(" const auto headerValue = $L;", headerValueExpression(element, "item", model)); + writer.write(" return acc.empty() ? headerValue : acc + \",\" + headerValue;"); + writer.write(" }));"); + } + + // Shared per-type header value expression, keyed on the target shape: enum → Mapper lookup, + // timestamp → the header timestamp mapping (epoch-seconds→Seconds(), date-time→ISO_8601, + // else→RFC822), primitive → to_string, any other (string) value used directly. The value + // expression (the field for a scalar member, the loop var for a list element) is the parameter, + // so the scalar and list paths share one copy of the enum-Mapper and timestamp-format mappings. + private static String headerValueExpression(Shape shape, String valueExpr, Model model) { + if (CppTypeMapper.isEnum(shape)) { + String enumType = CppTypeMapper.getCppType(shape, model, false); + return enumType + "Mapper::GetNameFor" + enumType + "(" + valueExpr + ")"; + } + if (shape.isTimestampShape()) { + String format = shape.getTrait(TimestampFormatTrait.class) + .map(TimestampFormatTrait::getValue) + .orElse("http-date"); + if (format.equals("epoch-seconds")) { + return "StringUtils::to_string(" + valueExpr + ".Seconds())"; + } + String dateFormat = format.equals("date-time") ? "ISO_8601" : "RFC822"; + return valueExpr + ".ToGmtString(Aws::Utils::DateFormat::" + dateFormat + ")"; + } + if (CppTypeMapper.isPrimitive(shape)) { + return "StringUtils::to_string(" + valueExpr + ")"; + } + return valueExpr; + } + + private static void renderScalarBody(CppWriter writer, String field, String location, Shape target, + Model model) { + if (target.isBooleanShape()) { + writer.write("ss << std::boolalpha << $L;", field); + emplaceFromStream(writer, location); + return; + } + if (target.isBlobShape()) { + writer.write("ss << HashingUtils::Base64Encode($L);", field); + emplaceFromStream(writer, location); + return; + } + if (target.isTimestampShape()) { + writer.write("headers.emplace(\"$L\", $L);", location, headerValueExpression(target, field, model)); + return; + } + // string / default scalar + writer.write("ss << $L;", field); + String value = location.equals("x-amz-copy-source") ? "URI::URLEncodePath(ss.str())" : "ss.str()"; + writer.write("headers.emplace(\"$L\", $L);", location, value); + writer.write("ss.str(\"\");"); + } + + private static void emplaceFromStream(CppWriter writer, String location) { + writer.write("headers.emplace(\"$L\", ss.str());", location); + writer.write("ss.str(\"\");"); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java new file mode 100644 index 00000000000..ce686fa91be --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java @@ -0,0 +1,187 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppNames; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.CustomizedAccessLogTagTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.HttpQueryParamsTrait; +import software.amazon.smithy.model.traits.HttpQueryTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; + +/** + * Emits the {@code @httpQuery} member-serialization loop body for a request (operation-input) + * structure's {@code AddQueryStringParameters(Aws::Http::URI&)}, byte-matching the legacy C2J + * {@code AddQueryStringParametersToRequest.vm}. + * + *

Protocol-agnostic: the query member serialization is byte-identical across REST-XML, + * Query-XML, EC2, JSON, and CBOR, so this renderer never branches on protocol. The caller + * ({@code ProtocolTraits.writeAddQueryStringParametersImpl}) owns the surrounding + * {@code Aws::StringStream ss;} declaration and the method scaffold; {@code uri} is the + * {@code Aws::Http::URI&} method parameter. + * + *

Every member is {@code HasBeenSet}-gated (C2J clears {@code required} on all members). Unlike + * headers, query enum members are NOT additionally guarded against {@code ::NOT_SET} — C2J's query + * template ({@code AddQueryStringParameter.vm}) gates on {@code HasBeenSet} only. + * + *

Scope: scalar / string / enum / timestamp {@code @httpQuery} members, {@code @httpQuery} + * lists (looped, one query parameter per element under the fixed location), and + * {@code @httpQueryParams} maps (looped, each entry keyed by the map's own key — scalar value, + * enum key via {@code Mapper}, or list value via an inner loop). Unlike headers, every query + * case routes its value through the shared {@code ss} stringstream. The query timestamp default + * is {@code date-time} (ISO_8601), differing from the header default (RFC822). + * + *

The S3 {@code customizedAccessLogTag} member (stamped with {@link CustomizedAccessLogTagTrait} + * by {@code S3Transforms}) is a special case: C2J models it with a {@code customizedQuery} flag, so + * it is skipped in the normal {@code @httpQueryParams} loop and instead emits an {@code x-}-prefix + * filter block once after the loop, byte-matching {@code AddQueryStringParametersToRequest.vm}. + */ +public final class RequestQuerySerializer { + + private RequestQuerySerializer() {} + + /** + * Emits the query-member serialization for every {@code @httpQuery} scalar/string/enum/timestamp + * member of {@code shape}, in model order. Members carrying no {@code @httpQuery} trait are skipped. + */ + public static void render(CppWriter writer, StructureShape shape, Model model) { + for (MemberShape member : shape.getAllMembers().values()) { + member.getTrait(HttpQueryTrait.class).ifPresent(trait -> + renderQueryMember(writer, member, trait.getValue(), model)); + member.getTrait(HttpQueryParamsTrait.class).ifPresent(trait -> { + // The S3 customizedAccessLogTag member carries @httpQueryParams (to keep the request + // emitting AddQueryStringParameters) but is not serialized as a normal map — it emits + // the x- filter block after this loop instead. + if (!member.hasTrait(CustomizedAccessLogTagTrait.class)) { + renderQueryParamsMap(writer, member, model); + } + }); + } + // C2J's customizedQuery block: emitted once, after the normal member loop, for the (single) + // marked S3 customizedAccessLogTag member. NOT HasBeenSet-gated. + for (MemberShape member : shape.getAllMembers().values()) { + if (member.hasTrait(CustomizedAccessLogTagTrait.class)) { + renderCustomizedAccessLogTagFilter(writer, member); + } + } + } + + // C2J's AddQueryStringParametersToRequest.vm customizedQuery block: keep only LogTags whose key + // starts with "x-", then add the collected map to the URI. Emitted byte-for-byte. + private static void renderCustomizedAccessLogTagFilter(CppWriter writer, MemberShape member) { + String field = CppNames.fieldName(member.getMemberName()); + writer.openBlock("if (!$L.empty()) {", "}", field, () -> { + writer.write("// only accept customized LogTag which starts with \"x-\""); + writer.write("Aws::Map collectedLogTags;"); + writer.openBlock("for (const auto& entry : $L) {", "}", field, () -> + writer.openBlock("if (!entry.first.empty() && !entry.second.empty() && " + + "entry.first.substr(0, 2) == \"x-\") {", "}", () -> + writer.write("collectedLogTags.emplace(entry.first, entry.second);"))); + writer.openBlock("if (!collectedLogTags.empty()) {", "}", () -> + writer.write("uri.AddQueryStringParameter(collectedLogTags);")); + }); + } + + private static void renderQueryMember(CppWriter writer, MemberShape member, String location, + Model model) { + Shape target = model.expectShape(member.getTarget()); + String field = CppNames.fieldName(member.getMemberName()); + + // @httpQuery list: one query parameter per element, all under the same fixed location. + if (target.isListShape()) { + Shape element = model.expectShape(target.asListShape().get().getMember().getTarget()); + writer.openBlock("if ($LHasBeenSet) {", "}", field, () -> + writer.openBlock("for (const auto& item : $L) {", "}", field, () -> { + writer.write("ss << $L;", elementStreamExpression("item", element, model)); + writer.write("uri.AddQueryStringParameter(\"$L\", ss.str());", location); + writer.write("ss.str(\"\");"); + })); + return; + } + + if (CppTypeMapper.isEnum(target)) { + String enumType = CppTypeMapper.getCppType(target, model, false); + // C2J's query template gates enums on HasBeenSet only (no NOT_SET clause, unlike headers). + writer.openBlock("if ($LHasBeenSet) {", "}", field, () -> + emitStreamed(writer, location, + enumType + "Mapper::GetNameFor" + enumType + "(" + field + ")")); + return; + } + + writer.openBlock("if ($LHasBeenSet) {", "}", field, () -> + emitStreamed(writer, location, elementStreamExpression(field, target, model))); + } + + private static void emitStreamed(CppWriter writer, String location, String streamExpression) { + writer.write("ss << $L;", streamExpression); + writer.write("uri.AddQueryStringParameter(\"$L\", ss.str());", location); + writer.write("ss.str(\"\");"); + } + + // @httpQueryParams map: each entry becomes a query parameter keyed by the map's own key + // (there is no fixed location). A scalar value streams directly; a list value fans out to one + // query parameter per element via an inner loop; an enum key is mapped through its Mapper. + private static void renderQueryParamsMap(CppWriter writer, MemberShape member, Model model) { + Shape target = model.expectShape(member.getTarget()); + String field = CppNames.fieldName(member.getMemberName()); + Shape key = model.expectShape(target.asMapShape().get().getKey().getTarget()); + Shape value = model.expectShape(target.asMapShape().get().getValue().getTarget()); + String keyExpression = queryParamKeyExpression(key, model); + + writer.openBlock("if ($LHasBeenSet) {", "}", field, () -> + writer.openBlock("for (auto& item : $L) {", "}", field, () -> { + if (value.isListShape()) { + Shape element = model.expectShape(value.asListShape().get().getMember().getTarget()); + writer.openBlock("for (auto& innerItem : item.second) {", "}", () -> { + writer.write("ss << $L;", elementStreamExpression("innerItem", element, model)); + writer.write("uri.AddQueryStringParameter($L, ss.str());", keyExpression); + writer.write("ss.str(\"\");"); + }); + } else { + writer.write("ss << $L;", elementStreamExpression("item.second", value, model)); + writer.write("uri.AddQueryStringParameter($L, ss.str());", keyExpression); + writer.write("ss.str(\"\");"); + } + })); + } + + // Query parameter key for an @httpQueryParams entry: an enum key routes through its Mapper, + // any other (string) key uses the raw entry key. Both terminate in .c_str() since + // AddQueryStringParameter takes a const char* key. + private static String queryParamKeyExpression(Shape key, Model model) { + if (CppTypeMapper.isEnum(key)) { + String enumType = CppTypeMapper.getCppType(key, model, false); + return enumType + "Mapper::GetNameFor" + enumType + "(item.first).c_str()"; + } + return "item.first.c_str()"; + } + + // Shared stream expression for a scalar member, list element, or map value: enum → Mapper + // lookup, timestamp → the query timestamp mapping (date-time→ISO_8601, http-date→RFC822, + // epoch-seconds→SecondsWithMSPrecision()), any other value streamed directly. The non-enum + // scalar path reuses this too (enum scalars are handled inline before reaching here). + private static String elementStreamExpression(String var, Shape target, Model model) { + if (CppTypeMapper.isEnum(target)) { + String enumType = CppTypeMapper.getCppType(target, model, false); + return enumType + "Mapper::GetNameFor" + enumType + "(" + var + ")"; + } + if (target.isTimestampShape()) { + String format = target.getTrait(TimestampFormatTrait.class) + .map(TimestampFormatTrait::getValue) + .orElse("date-time"); + if (format.equals("epoch-seconds")) { + return var + ".SecondsWithMSPrecision()"; + } + String dateFormat = format.equals("http-date") ? "RFC822" : "ISO_8601"; + return var + ".ToGmtString(Aws::Utils::DateFormat::" + dateFormat + ")"; + } + return var; + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java new file mode 100644 index 00000000000..cb40c23c71e --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java @@ -0,0 +1,28 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link S3Transforms} on the + * {@code customizedAccessLogTag} map member it injects onto every S3 request. C2J models that + * member with a distinct {@code customizedQuery} flag rather than an ordinary query-string map: + * its {@code AddQueryStringParametersToRequest.vm} skips the normal {@code @httpQueryParams} loop + * for it and instead emits the {@code x-}-prefix filter block ({@code collectedLogTags}). This + * marker preserves that distinction — {@code RequestQuerySerializer} skips the marked member in + * the normal map loop and emits the {@code x-} filter block for it after the loop. The member + * keeps its {@code @httpQueryParams} trait so the request still declares + * {@code AddQueryStringParameters} ({@code RequestBindings.hasQueryStringMembers}). + */ +public final class CustomizedAccessLogTagTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#customizedAccessLogTag"); + + public CustomizedAccessLogTagTrait() { + super(ID, Node.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 152635b1050..61fcfffdd21 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -194,9 +194,10 @@ private static Model markEmbeddedErrors(Model model) { } // C2J's S3RestXmlCppClientGenerator appends a `customizedAccessLogTag` map member - // to every operation request shape. It renders as an ordinary map member in the .h; the query- - // string binding (location=querystring, customizedQuery=true) is a serde concern deferred until - // Smithy serde lands, so no @httpQuery/@httpQueryParams trait is attached here. + // to every operation request shape, modeled with a distinct `customizedQuery` flag. It binds to + // the query string via @httpQueryParams (so every request emits AddQueryStringParameters), and + // additionally carries the CustomizedAccessLogTagTrait marker so RequestQuerySerializer skips the + // normal map loop for it and instead emits C2J's x--prefix filter block. private static Model injectAccessLogTagQuery(Model model, ServiceShape service) { ShapeId mapId = ShapeId.fromParts("com.amazonaws.s3", "CustomizedAccessLogTag"); ShapeId stringId = ShapeId.from("smithy.api#String"); @@ -228,6 +229,9 @@ private static Model injectAccessLogTagQuery(Model model, ServiceShape service) // querystring member on every request, which is what makes every request emit // AddQueryStringParameters; the trait drives RequestBindings.hasQueryStringMembers. .addTrait(new HttpQueryParamsTrait()) + // Marker for C2J's customizedQuery flag: RequestQuerySerializer skips the normal + // map loop for this member and emits the x--prefix filter block instead. + .addTrait(new CustomizedAccessLogTagTrait()) .build()) .build()); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java index 01834ac14ae..2936fe29d35 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java @@ -391,12 +391,17 @@ void requestHeader_alwaysHasSerializePayload_neverSerdeIncludes(Protocol p) { @ParameterizedTest @EnumSource(value = Protocol.class, names = {"JSON"}) - void awsJson_request_hasTargetHeaderAndQueryAndSerialize(Protocol p) { + void awsJson_request_hasTargetHeaderAndSerialize_noWireBindings(Protocol p) { String h = file(p, "DoThingRequest.h"); assertTrue(h.contains("SerializePayload() const override;"), h); - assertTrue(h.contains("GetRequestSpecificHeaders() const override;"), h); // header member OR target - assertTrue(h.contains("AddQueryStringParameters(Aws::Http::URI& uri) const override;"), h); + assertTrue(h.contains("GetRequestSpecificHeaders() const override;"), h); // X-Amz-Target + // RPC awsJson routes @httpHeader/@httpQuery members to the body: no AddQueryStringParameters, + // and no member header serialization inside GetRequestSpecificHeaders. + assertFalse(h.contains("AddQueryStringParameters(Aws::Http::URI& uri) const override;"), h); assertFalse(h.contains("DumpBodyToUrl"), h); + String c = file(p, "DoThingRequest.cpp"); + assertTrue(c.contains("X-Amz-Target"), c); + assertFalse(c.contains("uri.AddQueryStringParameter"), c); } @ParameterizedTest diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraitsTest.java index 258c1d41361..4a1cea49663 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraitsTest.java @@ -175,6 +175,27 @@ void requestWithInput_emitsContentTypeAndEncoderPayload() { assertTrue(i.contains("headers.emplace(Aws::Http::ACCEPT_HEADER, Aws::CBOR_CONTENT_TYPE);"), i); } + @Test + void cbor_queryMember_notWireSerialized_keepsProtocolHeaders() { + // RPC CBOR routes @httpQuery members to the body; no AddQueryStringParameters method, but + // the fixed CBOR protocol headers are still emitted. + var req = reqWith(false, true); var op = opWithInput(req); var model = modelWith(req); + String d = render(w -> cbor.writeRequestMethodDecls(w, "AWS_EX_API", req, op, model)); + assertFalse(d.contains("AddQueryStringParameters"), d); + String i = render(w -> cbor.writeRequestMethodImpls(w, "DoThingRequest", req, op, svcAthena(), model)); + assertFalse(i.contains("AddQueryStringParameters"), i); + assertTrue(i.contains("headers.emplace(Aws::Http::SMITHY_PROTOCOL_HEADER, Aws::RPC_V2_CBOR);"), i); + } + + @Test + void cbor_headerMember_notWireSerialized_keepsProtocolHeaders() { + var req = reqWith(true, false); var op = opWithInput(req); var model = modelWith(req); + String i = render(w -> cbor.writeRequestMethodImpls(w, "DoThingRequest", req, op, svcAthena(), model)); + assertFalse(i.contains("headers.emplace(\"x-h\""), i); + assertFalse(i.contains("ss << m_h;"), i); + assertTrue(i.contains("headers.emplace(Aws::Http::ACCEPT_HEADER, Aws::CBOR_CONTENT_TYPE);"), i); + } + // hasRequest()==false (input targets smithy.api#Unit): SerializePayload returns {}, NO CONTENT_TYPE. @Test void noInputRequest_returnsEmptyBracesAndOmitsContentType() { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java index 7f4281bf8f2..9590f2f830d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java @@ -175,10 +175,42 @@ void restJson_withHeaderMember_emitsHeadersMethodWithoutTarget() { } @Test - void json_withQueryMember_emitsAddQueryStringParameters() { + void awsJson_headerMember_notWireSerialized_keepsTarget() { + // RPC awsJson routes @httpHeader members to the JSON body; GetRequestSpecificHeaders still + // emits X-Amz-Target but performs no member header serialization. + var req = reqWith(true, false); var model = modelWith(req); + String i = render(w -> json.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertTrue(i.contains("X-Amz-Target"), i); + assertFalse(i.contains("headers.emplace(\"x-h\""), i); + assertFalse(i.contains("ss << m_h;"), i); + } + + @Test + void awsJson_queryMember_notWireSerialized() { + // RPC awsJson routes @httpQuery members to the body; no AddQueryStringParameters is emitted. var req = reqWith(false, true); var model = modelWith(req); String d = render(w -> json.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThing(), model)); + assertFalse(d.contains("AddQueryStringParameters"), d); + String i = render(w -> json.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertFalse(i.contains("AddQueryStringParameters"), i); + } + + @Test + void restJson_headerMember_isWireSerialized() { + // REST protocols honor HTTP bindings: the @httpHeader member is serialized onto the wire. + var req = reqWith(true, false); var model = modelWith(req); + String i = render(w -> restJson.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertTrue(i.contains("if (m_hHasBeenSet) {"), i); + assertTrue(i.contains("headers.emplace(\"x-h\", ss.str());"), i); + } + + @Test + void restJson_queryMember_isWireSerialized() { + var req = reqWith(false, true); var model = modelWith(req); + String d = render(w -> restJson.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThing(), model)); assertTrue(d.contains("void AddQueryStringParameters(Aws::Http::URI& uri) const override;"), d); + String i = render(w -> restJson.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertTrue(i.contains("uri.AddQueryStringParameter(\"q\", ss.str());"), i); } @Test diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializerTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializerTest.java new file mode 100644 index 00000000000..e77a05d3b1d --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializerTest.java @@ -0,0 +1,317 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.traits.HttpHeaderTrait; +import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; +import software.amazon.smithy.model.traits.SparseTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies {@link RequestHeaderSerializer} emits the scalar {@code @httpHeader} member + * serialization (string / {@code x-amz-copy-source} / enum / boolean / blob / timestamp) + * byte-for-byte with the legacy C2J {@code ModelClassHeaderMembersSource.vm} output. + */ +class RequestHeaderSerializerTest { + + private static String render(StructureShape shape, Model model) { + CppWriter w = new CppWriter(); + RequestHeaderSerializer.render(w, shape, model); + return w.toString(); + } + + @Test + void stringHeader_gatedAndStreamed() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("CacheControl", s.getId(), b -> b.addTrait(new HttpHeaderTrait("cache-control"))) + .build(); + Model m = Model.builder().addShapes(s, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_cacheControlHasBeenSet) {"), out); + assertTrue(out.contains("ss << m_cacheControl;"), out); + assertTrue(out.contains("headers.emplace(\"cache-control\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void copySourceHeader_isUrlEncoded() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("CopySource", s.getId(), b -> b.addTrait(new HttpHeaderTrait("x-amz-copy-source"))) + .build(); + Model m = Model.builder().addShapes(s, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_copySourceHasBeenSet) {"), out); + assertTrue(out.contains("ss << m_copySource;"), out); + assertTrue(out.contains( + "headers.emplace(\"x-amz-copy-source\", URI::URLEncodePath(ss.str()));"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void enumHeader_usesMapperAndNotSetGuard() { + EnumShape acl = EnumShape.builder().id("com.ex#ObjectCannedACL") + .addMember("PRIVATE", "private") + .addMember("PUBLIC_READ", "public-read") + .build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("ACL", acl.getId(), b -> b.addTrait(new HttpHeaderTrait("x-amz-acl"))) + .build(); + Model m = Model.builder().addShapes(acl, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_aCLHasBeenSet && m_aCL != ObjectCannedACL::NOT_SET) {"), out); + assertTrue(out.contains( + "headers.emplace(\"x-amz-acl\", ObjectCannedACLMapper::GetNameForObjectCannedACL(m_aCL));"), out); + // Enums never route through the stringstream. + assertFalse(out.contains("ss << m_aCL"), out); + } + + @Test + void booleanHeader_usesBoolalpha() { + BooleanShape b = BooleanShape.builder().id("com.ex#B").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("BypassGovernanceRetention", b.getId(), + mb -> mb.addTrait(new HttpHeaderTrait("x-amz-bypass-governance-retention"))) + .build(); + Model m = Model.builder().addShapes(b, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_bypassGovernanceRetentionHasBeenSet) {"), out); + assertTrue(out.contains("ss << std::boolalpha << m_bypassGovernanceRetention;"), out); + assertTrue(out.contains( + "headers.emplace(\"x-amz-bypass-governance-retention\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void blobHeader_base64() { + BlobShape blob = BlobShape.builder().id("com.ex#Blob").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("SSECustomerKeyMD5", blob.getId(), + mb -> mb.addTrait(new HttpHeaderTrait("x-amz-server-side-encryption-customer-key-MD5"))) + .build(); + Model m = Model.builder().addShapes(blob, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_sSECustomerKeyMD5HasBeenSet) {"), out); + assertTrue(out.contains("ss << HashingUtils::Base64Encode(m_sSECustomerKeyMD5);"), out); + // C2J lowercases header locations: the model's uppercase "MD5" segment becomes "md5". + assertTrue(out.contains( + "headers.emplace(\"x-amz-server-side-encryption-customer-key-md5\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void timestampHeader_defaultRfc822() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Expires", ts.getId(), b -> b.addTrait(new HttpHeaderTrait("expires"))) + .build(); + Model m = Model.builder().addShapes(ts, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_expiresHasBeenSet) {"), out); + assertTrue(out.contains( + "headers.emplace(\"expires\", m_expires.ToGmtString(Aws::Utils::DateFormat::RFC822));"), out); + // Timestamps emplace directly; they never touch the stringstream. + assertFalse(out.contains("ss << m_expires"), out); + } + + @Test + void timestampHeader_dateTimeTraitMapsToIso8601() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T") + .addTrait(new TimestampFormatTrait("date-time")).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Expires", ts.getId(), b -> b.addTrait(new HttpHeaderTrait("expires"))) + .build(); + Model m = Model.builder().addShapes(ts, req).build(); + String out = render(req, m); + assertTrue(out.contains( + "headers.emplace(\"expires\", m_expires.ToGmtString(Aws::Utils::DateFormat::ISO_8601));"), out); + } + + @Test + void epochSecondsTimestampHeader_usesSeconds() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T") + .addTrait(new TimestampFormatTrait("epoch-seconds")).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Expires", ts.getId(), b -> b.addTrait(new HttpHeaderTrait("expires"))) + .build(); + Model m = Model.builder().addShapes(ts, req).build(); + String out = render(req, m); + assertTrue(out.contains( + "headers.emplace(\"expires\", StringUtils::to_string(m_expires.Seconds()));"), out); + } + + @Test + void prefixHeadersMap_loopsWithPrefix() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + MapShape map = MapShape.builder().id("com.ex#Meta") + .key(s.getId()).value(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Metadata", map.getId(), + b -> b.addTrait(new HttpPrefixHeadersTrait("x-amz-meta-"))) + .build(); + Model m = Model.builder().addShapes(s, map, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_metadataHasBeenSet) {"), out); + assertTrue(out.contains("for (const auto& item : m_metadata) {"), out); + assertTrue(out.contains("ss << \"x-amz-meta-\" << item.first;"), out); + assertTrue(out.contains("headers.emplace(ss.str(), item.second);"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void sparsePrefixHeadersMap_unwrapsOptional() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + MapShape map = MapShape.builder().id("com.ex#Meta") + .key(s.getId()).value(s.getId()) + .addTrait(new SparseTrait()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Metadata", map.getId(), + b -> b.addTrait(new HttpPrefixHeadersTrait("x-amz-meta-"))) + .build(); + Model m = Model.builder().addShapes(s, map, req).build(); + String out = render(req, m); + assertTrue(out.contains("for (const auto& item : m_metadata) {"), out); + assertTrue(out.contains("ss << \"x-amz-meta-\" << item.first;"), out); + assertTrue(out.contains( + "if (item.second.has_value()) { headers.emplace(ss.str(), item.second.value()); }"), out); + // The sparse form never emits the plain unconditional emplace. + assertFalse(out.contains("headers.emplace(ss.str(), item.second);"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void listHeader_accumulatesCommaJoined() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("X", list.getId(), b -> b.addTrait(new HttpHeaderTrait("x-h"))) + .build(); + Model m = Model.builder().addShapes(s, list, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_xHasBeenSet) {"), out); + assertTrue(out.contains( + "headers.emplace(\"x-h\", std::accumulate(std::begin(m_x), std::end(m_x), Aws::String{},"), out); + assertTrue(out.contains( + "[](const Aws::String& acc, const Aws::String& item) -> Aws::String {"), out); + assertTrue(out.contains("const auto headerValue = item;"), out); + assertTrue(out.contains("return acc.empty() ? headerValue : acc + \",\" + headerValue;"), out); + assertTrue(out.contains("}));"), out); + } + + @Test + void listHeader_enumElement_usesMapper() { + EnumShape e = EnumShape.builder().id("com.ex#ObjectAttributes") + .addMember("ETAG", "ETag").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(e.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("X", list.getId(), + b -> b.addTrait(new HttpHeaderTrait("x-amz-object-attributes"))) + .build(); + Model m = Model.builder().addShapes(e, list, req).build(); + String out = render(req, m); + assertTrue(out.contains( + "[](const Aws::String& acc, const ObjectAttributes& item) -> Aws::String {"), out); + assertTrue(out.contains( + "const auto headerValue = ObjectAttributesMapper::GetNameForObjectAttributes(item);"), out); + } + + @Test + void listHeader_timestampElement_usesToGmtString() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(ts.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("X", list.getId(), b -> b.addTrait(new HttpHeaderTrait("x-h"))) + .build(); + Model m = Model.builder().addShapes(ts, list, req).build(); + String out = render(req, m); + assertTrue(out.contains( + "[](const Aws::String& acc, const Aws::Utils::DateTime& item) -> Aws::String {"), out); + assertTrue(out.contains( + "const auto headerValue = item.ToGmtString(Aws::Utils::DateFormat::RFC822);"), out); + } + + @Test + void listHeader_primitiveElement_usesToString() { + IntegerShape i = IntegerShape.builder().id("com.ex#I").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(i.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("X", list.getId(), b -> b.addTrait(new HttpHeaderTrait("x-h"))) + .build(); + Model m = Model.builder().addShapes(i, list, req).build(); + String out = render(req, m); + assertTrue(out.contains( + "[](const Aws::String& acc, const int& item) -> Aws::String {"), out); + assertTrue(out.contains("const auto headerValue = StringUtils::to_string(item);"), out); + } + + @Test + void mixedCaseHeaderLocation_isLowercased() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("CacheControl", s.getId(), b -> b.addTrait(new HttpHeaderTrait("Cache-Control"))) + .build(); + Model m = Model.builder().addShapes(s, req).build(); + String out = render(req, m); + // C2J lowercases HTTP header location names; the mixed-case model name must be emitted lowercase. + assertTrue(out.contains("headers.emplace(\"cache-control\", ss.str());"), out); + assertFalse(out.contains("Cache-Control"), out); + } + + @Test + void mixedCaseEnumHeaderLocation_isLowercased() { + EnumShape acl = EnumShape.builder().id("com.ex#ObjectCannedACL") + .addMember("PRIVATE", "private") + .build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("ACL", acl.getId(), b -> b.addTrait(new HttpHeaderTrait("X-Amz-ACL"))) + .build(); + Model m = Model.builder().addShapes(acl, req).build(); + String out = render(req, m); + assertTrue(out.contains( + "headers.emplace(\"x-amz-acl\", ObjectCannedACLMapper::GetNameForObjectCannedACL(m_aCL));"), out); + assertFalse(out.contains("X-Amz-ACL"), out); + } + + @Test + void mixedCasePrefixHeaders_prefixIsLowercased() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + MapShape map = MapShape.builder().id("com.ex#Meta") + .key(s.getId()).value(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Metadata", map.getId(), + b -> b.addTrait(new HttpPrefixHeadersTrait("X-Amz-Meta-"))) + .build(); + Model m = Model.builder().addShapes(s, map, req).build(); + String out = render(req, m); + assertTrue(out.contains("ss << \"x-amz-meta-\" << item.first;"), out); + assertFalse(out.contains("X-Amz-Meta-"), out); + } + + @Test + void memberWithoutHeaderTrait_emitsNothing() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Body", s.getId()) + .build(); + Model m = Model.builder().addShapes(s, req).build(); + assertTrue(render(req, m).isBlank(), render(req, m)); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializerTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializerTest.java new file mode 100644 index 00000000000..5d5b53fa0b5 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializerTest.java @@ -0,0 +1,268 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.traits.HttpQueryParamsTrait; +import software.amazon.smithy.model.traits.HttpQueryTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies {@link RequestQuerySerializer} emits the scalar / string / enum / timestamp + * {@code @httpQuery} member serialization byte-for-byte with the legacy C2J + * {@code AddQueryStringParametersToRequest.vm} output. {@code @httpQuery} lists and + * {@code @httpQueryParams} maps are handled separately and are not exercised here. + */ +class RequestQuerySerializerTest { + + private static String render(StructureShape shape, Model model) { + CppWriter w = new CppWriter(); + RequestQuerySerializer.render(w, shape, model); + return w.toString(); + } + + @Test + void stringQuery_gatedAndAdded() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Prefix", s.getId(), b -> b.addTrait(new HttpQueryTrait("prefix"))) + .build(); + Model m = Model.builder().addShapes(s, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_prefixHasBeenSet) {"), out); + assertTrue(out.contains("ss << m_prefix;"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(\"prefix\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void enumQuery_streamsMapper() { + EnumShape e = EnumShape.builder().id("com.ex#EncodingType") + .addMember("URL", "url") + .build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("EncodingType", e.getId(), b -> b.addTrait(new HttpQueryTrait("encoding-type"))) + .build(); + Model m = Model.builder().addShapes(e, req).build(); + String out = render(req, m); + // C2J's query template (AddQueryStringParameter.vm) gates query enums on HasBeenSet only — + // there is no NOT_SET clause for query members (unlike headers). + assertTrue(out.contains("if (m_encodingTypeHasBeenSet) {"), out); + assertFalse(out.contains("NOT_SET"), out); + assertTrue(out.contains( + "ss << EncodingTypeMapper::GetNameForEncodingType(m_encodingType);"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(\"encoding-type\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void timestampQuery_defaultIso8601() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Expires", ts.getId(), b -> b.addTrait(new HttpQueryTrait("expires"))) + .build(); + Model m = Model.builder().addShapes(ts, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_expiresHasBeenSet) {"), out); + assertTrue(out.contains( + "ss << m_expires.ToGmtString(Aws::Utils::DateFormat::ISO_8601);"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(\"expires\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void timestampQuery_httpDateTraitMapsToRfc822() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T") + .addTrait(new TimestampFormatTrait("http-date")).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Expires", ts.getId(), b -> b.addTrait(new HttpQueryTrait("expires"))) + .build(); + Model m = Model.builder().addShapes(ts, req).build(); + String out = render(req, m); + assertTrue(out.contains( + "ss << m_expires.ToGmtString(Aws::Utils::DateFormat::RFC822);"), out); + } + + @Test + void timestampQuery_epochSecondsTrait() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T") + .addTrait(new TimestampFormatTrait("epoch-seconds")).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Expires", ts.getId(), b -> b.addTrait(new HttpQueryTrait("expires"))) + .build(); + Model m = Model.builder().addShapes(ts, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_expiresHasBeenSet) {"), out); + assertTrue(out.contains("ss << m_expires.SecondsWithMSPrecision();"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(\"expires\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void memberWithoutQueryTrait_emitsNothing() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Body", s.getId()) + .build(); + Model m = Model.builder().addShapes(s, req).build(); + assertTrue(render(req, m).isBlank(), render(req, m)); + } + + @Test + void listQuery_loopsSameLocation() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Ids", list.getId(), b -> b.addTrait(new HttpQueryTrait("id"))) + .build(); + Model m = Model.builder().addShapes(s, list, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_idsHasBeenSet) {"), out); + assertTrue(out.contains("for (const auto& item : m_ids) {"), out); + assertTrue(out.contains("ss << item;"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(\"id\", ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void listQuery_enumElement_streamsMapper() { + EnumShape e = EnumShape.builder().id("com.ex#EncodingType") + .addMember("URL", "url").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(e.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("EncodingTypes", list.getId(), b -> b.addTrait(new HttpQueryTrait("encoding-type"))) + .build(); + Model m = Model.builder().addShapes(e, list, req).build(); + String out = render(req, m); + assertTrue(out.contains("for (const auto& item : m_encodingTypes) {"), out); + assertTrue(out.contains("ss << EncodingTypeMapper::GetNameForEncodingType(item);"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(\"encoding-type\", ss.str());"), out); + } + + @Test + void listQuery_timestampElement_defaultIso8601() { + TimestampShape ts = TimestampShape.builder().id("com.ex#T").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(ts.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Expirations", list.getId(), b -> b.addTrait(new HttpQueryTrait("expires"))) + .build(); + Model m = Model.builder().addShapes(ts, list, req).build(); + String out = render(req, m); + assertTrue(out.contains("for (const auto& item : m_expirations) {"), out); + assertTrue(out.contains( + "ss << item.ToGmtString(Aws::Utils::DateFormat::ISO_8601);"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(\"expires\", ss.str());"), out); + } + + @Test + void queryParamsMap_scalarValue_usesEntryKey() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + MapShape map = MapShape.builder().id("com.ex#M").key(s.getId()).value(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Params", map.getId(), b -> b.addTrait(new HttpQueryParamsTrait())) + .build(); + Model m = Model.builder().addShapes(s, map, req).build(); + String out = render(req, m); + assertTrue(out.contains("if (m_paramsHasBeenSet) {"), out); + assertTrue(out.contains("for (auto& item : m_params) {"), out); + assertTrue(out.contains("ss << item.second;"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(item.first.c_str(), ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void queryParamsMap_listValue_innerLoop() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + ListShape list = ListShape.builder().id("com.ex#L").member(s.getId()).build(); + MapShape map = MapShape.builder().id("com.ex#M").key(s.getId()).value(list.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Params", map.getId(), b -> b.addTrait(new HttpQueryParamsTrait())) + .build(); + Model m = Model.builder().addShapes(s, list, map, req).build(); + String out = render(req, m); + assertTrue(out.contains("for (auto& item : m_params) {"), out); + assertTrue(out.contains("for (auto& innerItem : item.second) {"), out); + assertTrue(out.contains("ss << innerItem;"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(item.first.c_str(), ss.str());"), out); + assertTrue(out.contains("ss.str(\"\");"), out); + } + + @Test + void customizedAccessLogTagMarker_emitsXFilterBlockAndSkipsNormalMap() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + MapShape map = MapShape.builder().id("com.ex#M").key(s.getId()).value(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("customizedAccessLogTag", map.getId(), b -> b + .addTrait(new HttpQueryParamsTrait()) + .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms + .CustomizedAccessLogTagTrait())) + .build(); + Model m = Model.builder().addShapes(s, map, req).build(); + String out = render(req, m); + + // The x- filter block is emitted verbatim (NOT HasBeenSet-gated). + assertTrue(out.contains("if (!m_customizedAccessLogTag.empty()) {"), out); + assertTrue(out.contains("// only accept customized LogTag which starts with \"x-\""), out); + assertTrue(out.contains("Aws::Map collectedLogTags;"), out); + assertTrue(out.contains("for (const auto& entry : m_customizedAccessLogTag) {"), out); + assertTrue(out.contains( + "if (!entry.first.empty() && !entry.second.empty() && entry.first.substr(0, 2) == \"x-\") {"), + out); + assertTrue(out.contains("collectedLogTags.emplace(entry.first, entry.second);"), out); + assertTrue(out.contains("if (!collectedLogTags.empty()) {"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(collectedLogTags);"), out); + + // The marked member must be skipped in the normal @httpQueryParams loop. + assertFalse(out.contains("if (m_customizedAccessLogTagHasBeenSet) {"), out); + assertFalse(out.contains("for (auto& item : m_customizedAccessLogTag) {"), out); + } + + @Test + void plainQueryParamsMap_withoutMarker_stillNormalSerialization() { + StringShape s = StringShape.builder().id("com.ex#S").build(); + MapShape map = MapShape.builder().id("com.ex#M").key(s.getId()).value(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Params", map.getId(), b -> b.addTrait(new HttpQueryParamsTrait())) + .build(); + Model m = Model.builder().addShapes(s, map, req).build(); + String out = render(req, m); + + // A plain @httpQueryParams map (no marker) keeps the Task-4 normal map serialization. + assertTrue(out.contains("if (m_paramsHasBeenSet) {"), out); + assertTrue(out.contains("for (auto& item : m_params) {"), out); + assertTrue(out.contains("ss << item.second;"), out); + assertTrue(out.contains("uri.AddQueryStringParameter(item.first.c_str(), ss.str());"), out); + // No x- filter block for a plain map. + assertFalse(out.contains("collectedLogTags"), out); + } + + @Test + void queryParamsMap_enumKey_usesMapper() { + EnumShape key = EnumShape.builder().id("com.ex#KeyEnum") + .addMember("A", "a").build(); + StringShape s = StringShape.builder().id("com.ex#S").build(); + MapShape map = MapShape.builder().id("com.ex#M").key(key.getId()).value(s.getId()).build(); + StructureShape req = StructureShape.builder().id("com.ex#R") + .addMember("Params", map.getId(), b -> b.addTrait(new HttpQueryParamsTrait())) + .build(); + Model m = Model.builder().addShapes(key, s, map, req).build(); + String out = render(req, m); + assertTrue(out.contains("for (auto& item : m_params) {"), out); + assertTrue(out.contains("ss << item.second;"), out); + assertTrue(out.contains( + "uri.AddQueryStringParameter(KeyEnumMapper::GetNameForKeyEnum(item.first).c_str(), ss.str());"), out); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 827178f250a..9879cc9a88e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -437,6 +437,23 @@ void injectsCustomizedAccessLogTagIntoRequestAsStringMapAppendedLast() { "access-log tag member appended last"); } + @Test + void stampsCustomizedAccessLogTagMarkerAndKeepsQueryParams() { + Model m = accessLogModel(); + Model out = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); + + StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); + MemberShape tag = input.getMember("customizedAccessLogTag").orElseThrow(); + + // C2J models this member with a distinct customizedQuery flag; mirror that with the marker. + assertTrue(tag.hasTrait(CustomizedAccessLogTagTrait.class), + "injected member must carry the CustomizedAccessLogTag marker"); + // The marker is additive: @httpQueryParams must remain so the request still emits + // AddQueryStringParameters (RequestBindings.hasQueryStringMembers). + assertTrue(tag.hasTrait(software.amazon.smithy.model.traits.HttpQueryParamsTrait.class), + "customizedAccessLogTag must still carry @httpQueryParams alongside the marker"); + } + @Test void doesNotInjectCustomizedAccessLogTagIntoOutput() { Model m = accessLogModel(); From 40ad6dc7946db1e04e1d553cb2c0265e74f478fe Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 1 Sep 2026 16:47:09 -0400 Subject: [PATCH 34/44] glacier customizations and code cleanup --- .../generators/model/MemberRenderer.java | 3 +- .../generators/model/ModelCodegenPlugin.java | 4 +- .../model/protocol/JsonProtocolTraits.java | 8 +- .../model/protocol/ProtocolTraits.java | 9 + .../protocol/QueryXmlProtocolTraits.java | 10 +- .../AdditionalRequestHeadersTrait.java | 52 ++++++ .../model/transforms/Ec2Transforms.java | 13 +- .../model/transforms/GlacierTransforms.java | 104 +++++++++++ .../model/transforms/S3Transforms.java | 66 +++---- .../model/transforms/SqsTransforms.java | 14 +- .../model/transforms/TransformSupport.java | 40 +++++ .../protocol/JsonProtocolTraitsTest.java | 26 +++ .../ProtocolTraitsIncludeSetTest.java | 16 ++ .../transforms/GlacierTransformsTest.java | 162 ++++++++++++++++++ .../transforms/TransformSupportTest.java | 40 +++++ 15 files changed, 491 insertions(+), 76 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java index 72bdf30ea40..d12bfbe576f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java @@ -20,6 +20,7 @@ import software.amazon.smithy.model.traits.StreamingTrait; import java.util.Map; +import java.util.Optional; /** * Renders C++ accessor methods (Get/Set/With/Add) and private member fields @@ -127,7 +128,7 @@ public void renderPublicAccessors(CppWriter writer) { } else { // S3 checksum members (stamped by S3Transforms) also select the ChecksumAlgorithm enum // in their setter, matching C2J's ModelClassMembersAndInlines.vm isChecksumMember path. - java.util.Optional checksum = member.getTrait(ChecksumMemberTrait.class); + Optional checksum = member.getTrait(ChecksumMemberTrait.class); writer.write("template ", templateParam, cppType); writer.openBlock("void Set$L($L&& value) {", "}", methodName, templateParam, () -> { writer.write("$LHasBeenSet = true;", fieldName); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index 5fe596ffbec..0079380c411 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -11,6 +11,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayV2Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.DynamoDbTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.Ec2Transforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlacierTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LambdaTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.S3ControlTransforms; @@ -62,7 +63,8 @@ public void execute(PluginContext context) { AccessAnalyzerTransforms.asTransform(), DynamoDbTransforms.asTransform(), S3Transforms.asTransform(), - S3ControlTransforms.asTransform() + S3ControlTransforms.asTransform(), + GlacierTransforms.asTransform() )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java index eace0615ab8..fac78e66949 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java @@ -88,9 +88,10 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of(); - // Request sources additionally serialize @httpQuery members via the shared query - // serializer, which needs URI (AddQueryStringParameter) and StringUtils. These are - // added only here to avoid widening the other source kinds. + // Request sources additionally serialize @httpHeader/@httpQuery members via the shared + // serializers, which need URI (AddQueryStringParameter), StringUtils, and + // (std::accumulate for comma-joined list @httpHeader members). These are added only + // here to avoid widening the other source kinds. case REQUEST_SOURCE: return List.of( "aws/core/utils/json/JsonSerializer.h", @@ -99,6 +100,7 @@ public List serdeIncludes(FileKind kind) { "aws/core/utils/HashingUtils.h", "aws/core/utils/StringUtils.h", "aws/core/http/URI.h", + "numeric", "utility"); // All source kinds share one union (supersets allowed: a .cpp may carry an // include it doesn't strictly use). Usings are unchanged; only #includes widen. diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java index 4954d7fe277..f144e81d2e1 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java @@ -9,6 +9,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestHeaderSerializer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestQuerySerializer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.AdditionalRequestHeadersTrait; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; @@ -205,6 +206,14 @@ default void writeGetRequestSpecificHeadersImpl(CppWriter writer, String classNa writer.write("headers.insert(Aws::Http::HeaderValuePair(\"X-Amz-Target\", \"$L.$L\"));", service.getId().getName(), operation.getId().getName()); } + // Per-service constant request headers (C2J metadata.additionalHeaders, e.g. Glacier's + // x-amz-glacier-version). Streaming requests derive from AmazonStreamingWebServiceRequest + // and bypass Request::GetHeaders, so these are emitted here — matching + // StreamRequestSource.vm, which inserts them after X-Amz-Target and before the + // member-driven headers. The trait is stamped only on request shapes that need it. + shape.getTrait(AdditionalRequestHeadersTrait.class).ifPresent(trait -> + trait.getHeaders().forEach((name, value) -> + writer.write("headers.insert(Aws::Http::HeaderValuePair(\"$L\", \"$L\"));", name, value))); // C2J declares the stringstream once, immediately after `headers`, whenever the request // has ≥1 header member (even all-enum/timestamp members, which never use it). RPC // protocols route HTTP-binding members to the body, so neither the stringstream nor the diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java index 4fdd005e202..2d6be30a89c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java @@ -95,9 +95,10 @@ public List serdeIncludes(FileKind kind) { case RESULT_HEADER: // Query/EC2 result headers forward-declare XmlDocument; no serde include. return List.of(); - // Request sources additionally serialize @httpQuery members via the shared query - // serializer, which needs URI (AddQueryStringParameter), StringUtils, and the - // stringstream. URI.h is added only here to avoid widening the other source kinds. + // Request sources additionally serialize @httpHeader/@httpQuery members via the shared + // serializers, which need URI (AddQueryStringParameter), StringUtils, the stringstream, + // and (std::accumulate for comma-joined list @httpHeader members). URI.h is + // added only here to avoid widening the other source kinds. case REQUEST_SOURCE: return List.of( "aws/core/utils/xml/XmlSerializer.h", @@ -106,7 +107,8 @@ public List serdeIncludes(FileKind kind) { "aws/core/utils/StringUtils.h", "aws/core/utils/memory/stl/AWSStringStream.h", "aws/core/utils/HashingUtils.h", - "aws/core/http/URI.h"); + "aws/core/http/URI.h", + "numeric"); case SUBOBJECT_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java new file mode 100644 index 00000000000..5abe1607c67 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java @@ -0,0 +1,52 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Internal marker (never declared in any model file) placed by {@link GlacierTransforms} on each + * request structure that C2J attaches constant request headers to via + * {@code metadata.setAdditionalHeaders(...)}. It carries the ordered header name → value pairs + * (for Glacier, {@code x-amz-glacier-version} → the service API version). + * + *

C2J emits these headers from the {@code Request} base class ({@code GetHeaders}) for + * ordinary requests, but a streaming request derives from {@code AmazonStreamingWebServiceRequest} + * and bypasses that base, so {@code StreamRequestSource.vm} instead emits them inside the request's + * own {@code GetRequestSpecificHeaders}. The base class stays C2J-generated, so only the streaming + * requests carry this marker; request rendering turns it into the matching {@code headers.insert(...)} + * lines. Kept as a data-carrying marker + generic renderer rule so the renderer stays + * service-agnostic. + */ +public final class AdditionalRequestHeadersTrait extends AbstractTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#additionalRequestHeaders"); + + private final Map headers; + + public AdditionalRequestHeadersTrait(Map headers) { + super(ID, SourceLocation.NONE); + this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); + } + + /** Ordered header name → value pairs, emitted verbatim into {@code GetRequestSpecificHeaders}. */ + public Map getHeaders() { + return headers; + } + + @Override + protected Node createNode() { + ObjectNode.Builder builder = Node.objectNodeBuilder(); + headers.forEach(builder::withMember); + return builder.build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java index 49d7cd1607a..c15df915a39 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java @@ -10,10 +10,8 @@ import software.amazon.smithy.model.shapes.BlobShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.SensitiveTrait; import software.amazon.smithy.model.transform.ModelTransformer; @@ -146,15 +144,6 @@ private static Model renameResultShapesToResponse(Model model) { } private static Model addSpotInstanceStateDisabled(Model model) { - Optional enumShape = model.shapes() - .filter(s -> "SpotInstanceState".equals(s.getId().getName())) - .filter(s -> s.isEnumShape() || s.hasTrait(EnumTrait.class)) - .findFirst(); - if (enumShape.isEmpty()) { - return model; - } - return TransformSupport.appendValues(enumShape.get(), List.of("disabled")) - .map(updated -> model.toBuilder().addShape(updated).build()) - .orElse(model); + return TransformSupport.appendEnumValuesByName(model, "SpotInstanceState", List.of("disabled")); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java new file mode 100644 index 00000000000..bab9d7fe54f --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java @@ -0,0 +1,104 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.HttpHeaderTrait; +import software.amazon.smithy.model.traits.HttpQueryTrait; +import software.amazon.smithy.model.transform.ModelTransformer; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Glacier parity with the legacy C2J {@code GlacierRestJsonCppClientGenerator} for the + * {@code Model::} namespace. C2J sets {@code metadata.additionalHeaders} to + * {@code {x-amz-glacier-version: }}, which the {@code Request} base class emits + * for ordinary requests. That base stays C2J-generated, so the only gap in the Smithy-generated + * model is the streaming requests ({@code UploadArchive}, {@code UploadMultipartPart}): they derive + * from {@code AmazonStreamingWebServiceRequest} and bypass the base {@code GetHeaders}, so C2J's + * {@code StreamRequestSource.vm} emits the constant header inside their own + * {@code GetRequestSpecificHeaders}. This stamps {@link AdditionalRequestHeadersTrait} on those + * streaming request inputs; request rendering turns it into the matching {@code headers.insert(...)}. + * Self-guards on the raw smithy service name and no-ops when the model has no streaming request. + */ +public final class GlacierTransforms { + + private static final String GLACIER_VERSION_HEADER = "x-amz-glacier-version"; + + private GlacierTransforms() {} + + public static ModelTransform asTransform() { + return GlacierTransforms::apply; + } + + private static Model apply(Model model, ServiceShape service) { + if (!"glacier".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { + return model; + } + return retypeLimitQueryMembersToString(addAdditionalHeaders(model, service), service); + } + + private static Model addAdditionalHeaders(Model model, ServiceShape service) { + Map additionalHeaders = new LinkedHashMap<>(); + additionalHeaders.put(GLACIER_VERSION_HEADER, service.getVersion()); + + List marked = new ArrayList<>(); + for (OperationShape operation : TopDownIndex.of(model).getContainedOperations(service)) { + if (ShapeClassifier.isRawStreamingPayloadRequest(operation, model)) { + StructureShape input = model.expectShape(operation.getInputShape(), StructureShape.class); + if (!input.hasTrait(AdditionalRequestHeadersTrait.class)) { + marked.add(input.toBuilder() + .addTrait(new AdditionalRequestHeadersTrait(additionalHeaders)).build()); + } + } + } + if (marked.isEmpty()) { + return model; // no streaming request present (idempotent / trimmed model). + } + return model.toBuilder().addShapes(marked.toArray(new Shape[0])).build(); + } + + // Upstream Coral2Smithy's GlacierTransformer retypes every header/query `limit` (page-size) member + // from string to smithy.api#Integer, arguing the wire form (a query param) is unchanged. But the + // C++ SDK historically shipped these as Aws::String, so consuming the integer would break the + // public API (Aws::String GetLimit() -> int GetLimit()). This inverts the upstream retype for the + // header/query `limit` members, retargeting them back to the service string shape — matching C2J + // and the sibling string members (e.g. marker). Only these page-size members are query/header + // bound; body `limit` members (whose type change would alter serialization) are never retyped by + // Coral2Smithy and so are already string. Pagination is unaffected: the paginators continue via + // the `Marker` continuation token and never read or write `limit`. + private static Model retypeLimitQueryMembersToString(Model model, ServiceShape service) { + ShapeId stringTarget = ShapeId.fromParts(service.getId().getNamespace(), "string"); + if (!model.getShape(stringTarget).isPresent()) { + throw new IllegalStateException( + "Expected service string shape " + stringTarget + " to retype Glacier limit members"); + } + Set replacements = model.shapes(MemberShape.class) + .filter(member -> member.getMemberName().equals("limit")) + .filter(member -> member.hasTrait(HttpQueryTrait.ID) || member.hasTrait(HttpHeaderTrait.ID)) + .filter(member -> !model.expectShape(member.getTarget()).isStringShape()) + .map(member -> member.toBuilder().target(stringTarget).build()) + .collect(Collectors.toSet()); + if (replacements.isEmpty()) { + return model; // already string (idempotent / upstream stopped retyping). + } + return ModelTransformer.create().replaceShapes(model, new ArrayList<>(replacements)); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 61fcfffdd21..4abbb61b85e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -23,7 +23,6 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.traits.DocumentationTrait; -import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.HttpHeaderTrait; import software.amazon.smithy.model.traits.HttpQueryParamsTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; @@ -115,16 +114,17 @@ private static Model markChecksumMembers(Model model, ServiceShape service) { CHECKSUM_MEMBERS_ENUMS.containsKey(m.getTarget().getName()) && !m.hasTrait(ChecksumMemberTrait.class)); if (needsStamp) { - StructureShape.Builder b = StructureShape.builder().id(req.getId()); - req.getAllTraits().values().forEach(b::addTrait); + // Re-add only the checksum members with the marker; addMember replaces in place, so + // the other members (and the shape's traits/source) carry over untouched via toBuilder. + StructureShape.Builder b = req.toBuilder(); for (MemberShape m : req.getAllMembers().values()) { String enumValue = CHECKSUM_MEMBERS_ENUMS.get(m.getTarget().getName()); - b.addMember(m.getMemberName(), m.getTarget(), mb -> { - m.getAllTraits().values().forEach(mb::addTrait); - if (enumValue != null && !m.hasTrait(ChecksumMemberTrait.class)) { + if (enumValue != null && !m.hasTrait(ChecksumMemberTrait.class)) { + b.addMember(m.getMemberName(), m.getTarget(), mb -> { + m.getAllTraits().values().forEach(mb::addTrait); mb.addTrait(new ChecksumMemberTrait(enumValue)); - } - }); + }); + } } replacements.add(b.build()); } @@ -275,16 +275,8 @@ private static Model normalizeReplicationStatus(Model model) { private static final List MISSING_REGIONS = List.of("us-iso-west-1", "us-east-1"); private static Model expandBucketLocationConstraint(Model model) { - Optional enumShape = model.shapes() - .filter(s -> "BucketLocationConstraint".equals(s.getId().getName())) - .filter(s -> s.isEnumShape() || s.hasTrait(EnumTrait.class)) - .findFirst(); - if (enumShape.isEmpty()) { - return model; - } - return TransformSupport.appendEnumValues(enumShape.get(), regionNameValueMap()) - .map(updated -> model.toBuilder().addShape(updated).build()) - .orElse(model); + return TransformSupport.appendEnumEntriesByName( + model, "BucketLocationConstraint", regionNameValueMap()); } private static Map regionNameValueMap() { @@ -313,14 +305,11 @@ private static Model hackGetObjectResult(Model model) { ShapeId id2ShapeId = ShapeId.fromParts(ns, "ObjectId2"); StringShape id2Shape = StringShape.builder().id(id2ShapeId).build(); - StructureShape.Builder b = StructureShape.builder().id(output.getId()); - output.getAllTraits().values().forEach(b::addTrait); - output.getAllMembers().values().forEach(m -> - b.addMember(m.getMemberName(), m.getTarget(), - mb -> m.getAllTraits().values().forEach(mb::addTrait))); - b.addMember("Id2", id2ShapeId, mb -> mb.addTrait(new HttpHeaderTrait("x-amz-id-2"))); + StructureShape withId2 = output.toBuilder() + .addMember("Id2", id2ShapeId, mb -> mb.addTrait(new HttpHeaderTrait("x-amz-id-2"))) + .build(); - return model.toBuilder().addShapes(id2Shape, b.build()).build(); + return model.toBuilder().addShapes(id2Shape, withId2).build(); } // C2J renames both the CopyObjectResult domain shape (to CopyObjectResultDetails) and the @@ -380,23 +369,16 @@ private static Model addExpiresCustomization(Model model, ServiceShape service) // Only customize structs that lack ExpiresString (idempotent). if (struct.getMember("ExpiresString").isEmpty()) { MemberShape expires = struct.getAllMembers().get("Expires"); - StructureShape.Builder b = StructureShape.builder().id(struct.getId()); - struct.getAllTraits().values().forEach(b::addTrait); - for (MemberShape m : struct.getAllMembers().values()) { - if (m.getMemberName().equals("Expires")) { - // Rewrite Expires' documentation to prepend the deprecation note. - String existingDoc = m.getTrait(DocumentationTrait.class) - .map(DocumentationTrait::getValue).orElse(""); - b.addMember("Expires", m.getTarget(), mb -> { - m.getAllTraits().values().forEach(mb::addTrait); - if (!existingDoc.toLowerCase().contains("deprecated")) { - mb.addTrait(new DocumentationTrait(EXPIRES_DEPRECATION + existingDoc)); - } - }); - } else { - b.addMember(m.getMemberName(), m.getTarget(), - mb -> m.getAllTraits().values().forEach(mb::addTrait)); - } + StructureShape.Builder b = struct.toBuilder(); + // Prepend the deprecation note to Expires' documentation (idempotent); addMember + // replaces the member in place, and adding a DocumentationTrait supersedes the old one. + String existingDoc = expires.getTrait(DocumentationTrait.class) + .map(DocumentationTrait::getValue).orElse(""); + if (!existingDoc.toLowerCase().contains("deprecated")) { + b.addMember("Expires", expires.getTarget(), mb -> { + expires.getAllTraits().values().forEach(mb::addTrait); + mb.addTrait(new DocumentationTrait(EXPIRES_DEPRECATION + existingDoc)); + }); } // Add ExpiresString cloning Expires' traits (so it reads the same header), retargeted. b.addMember("ExpiresString", expiresStringId, diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java index 68d55f1c46e..4c6698b8f6c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java @@ -8,11 +8,8 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.traits.EnumTrait; import java.util.List; -import java.util.Optional; /** * Adds the unmodeled {@code QueueAttributeName} enum values that the legacy C2J @@ -35,15 +32,6 @@ private static Model apply(Model model, ServiceShape service) { if (!"sqs".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { return model; } - Optional target = model.shapes() - .filter(s -> ENUM_NAME.equals(s.getId().getName())) - .filter(s -> s.isEnumShape() || s.hasTrait(EnumTrait.class)) - .findFirst(); - if (target.isEmpty()) { - return model; - } - return TransformSupport.appendValues(target.get(), ADDED_VALUES) - .map(updated -> model.toBuilder().addShape(updated).build()) - .orElse(model); + return TransformSupport.appendEnumValuesByName(model, ENUM_NAME, ADDED_VALUES); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java index 54c585aa68c..3d6df1ca2cf 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java @@ -7,6 +7,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.EnumRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait; +import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; @@ -142,6 +143,45 @@ static Optional appendEnumValues(Shape enumShape, Map nam .build()); } + /** + * Locates an enum shape by its simple (relative) name and appends the given identifier-safe wire + * {@code values}, returning the model with the updated shape — or unchanged when the shape is + * absent or every value is already present. Wraps the per-service pattern of adding unmodeled + * enum values; see {@link #appendValues} for the identifier-safe precondition on {@code values}. + * + *

The lookup matches the first shape whose relative name equals {@code simpleName} and which + * is an enum (Smithy 2.0 {@code EnumShape} or a legacy {@code StringShape} with an {@code @enum} + * trait). Callers are expected to have already scoped generation to a single service. + */ + static Model appendEnumValuesByName(Model model, String simpleName, List values) { + return findEnumByName(model, simpleName) + .flatMap(shape -> appendValues(shape, values)) + .map(updated -> model.toBuilder().addShape(updated).build()) + .orElse(model); + } + + /** + * Locates an enum shape by its simple (relative) name and appends the given {@code member-name -> + * wire-value} entries (allowing non-identifier-safe wire values, e.g. region strings), returning + * the model with the updated shape — or unchanged when the shape is absent or every value is + * already present. Wraps the per-service pattern; see {@link #appendEnumValues} for the + * member-name precondition and value semantics. + */ + static Model appendEnumEntriesByName(Model model, String simpleName, Map nameToValue) { + return findEnumByName(model, simpleName) + .flatMap(shape -> appendEnumValues(shape, nameToValue)) + .map(updated -> model.toBuilder().addShape(updated).build()) + .orElse(model); + } + + /** The first enum shape (Smithy 2.0 {@code EnumShape} or legacy {@code @enum}) whose relative name matches. */ + private static Optional findEnumByName(Model model, String simpleName) { + return model.shapes() + .filter(s -> simpleName.equals(s.getId().getName())) + .filter(s -> s.isEnumShape() || s.hasTrait(EnumTrait.class)) + .findFirst(); + } + /** The current wire values of an enum shape (Smithy 2.0 {@code EnumShape} or legacy {@code @enum}). */ private static List existingWireValues(Shape enumShape) { if (enumShape.isEnumShape()) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java index 9590f2f830d..1a1957d5568 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java @@ -204,6 +204,32 @@ void restJson_headerMember_isWireSerialized() { assertTrue(i.contains("headers.emplace(\"x-h\", ss.str());"), i); } + @Test + void restJson_additionalHeadersTrait_emitsConstantHeaderBeforeMemberHeaders() { + // A streaming request marked with AdditionalRequestHeadersTrait (Glacier's + // x-amz-glacier-version) emits the constant header inside GetRequestSpecificHeaders, + // ordered after any X-Amz-Target and before the member-driven headers (StreamRequestSource.vm). + var req = reqWith(true, false).toBuilder() + .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms + .AdditionalRequestHeadersTrait(java.util.Map.of("x-amz-glacier-version", "2012-06-01"))) + .build(); + var model = modelWith(req); + String i = render(w -> restJson.writeRequestMethodImpls( + w, "UploadArchiveRequest", req, opDoThing(), svcAthena(), model)); + assertTrue(i.contains( + "headers.insert(Aws::Http::HeaderValuePair(\"x-amz-glacier-version\", \"2012-06-01\"));"), i); + assertTrue(i.indexOf("x-amz-glacier-version") < i.indexOf("Aws::StringStream ss;"), + "constant header precedes the member-driven headers: " + i); + } + + @Test + void restJson_noAdditionalHeadersTrait_emitsNoConstantHeader() { + var req = reqWith(true, false); var model = modelWith(req); + String i = render(w -> restJson.writeRequestMethodImpls( + w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertFalse(i.contains("x-amz-glacier-version"), i); + } + @Test void restJson_queryMember_isWireSerialized() { var req = reqWith(false, true); var model = modelWith(req); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java index d0b31058eba..e9a42e13787 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java @@ -47,6 +47,22 @@ void queryXml_subobjectHeader_hasStreamFwd() { q.serdeIncludes(FileKind.SUBOBJECT_HEADER).toString()); } + @Test + void requestSource_everyProtocolIncludesNumericForListHeaderAccumulate() { + // RequestHeaderSerializer emits std::accumulate for list-typed @httpHeader members and is + // protocol-agnostic, so every protocol's REQUEST_SOURCE must declare . Guards the + // JSON/QueryXml regression where only RestXml carried it (relying on transitive includes). + for (ProtocolTraits t : List.of( + new JsonProtocolTraits(Protocol.JSON), + new JsonProtocolTraits(Protocol.REST_JSON), + new QueryXmlProtocolTraits(Protocol.QUERY_XML), + new QueryXmlProtocolTraits(Protocol.EC2), + new RestXmlProtocolTraits())) { + List inc = t.serdeIncludes(FileKind.REQUEST_SOURCE); + assertTrue(inc.contains("numeric"), t.protocol() + " REQUEST_SOURCE: " + inc); + } + } + @Test void restXml_requestSource_hasUtilityAndXmlSerializer() { ProtocolTraits x = new RestXmlProtocolTraits(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java new file mode 100644 index 00000000000..2aba1d64a30 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java @@ -0,0 +1,162 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.HttpPayloadTrait; +import software.amazon.smithy.model.traits.HttpQueryTrait; +import software.amazon.smithy.model.traits.StreamingTrait; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GlacierTransformsTest { + + static final String NS = "com.amazonaws.glacier"; + + private static ServiceShape glacierService(String sdkId, OperationShape... operations) { + ServiceShape.Builder b = ServiceShape.builder().id(NS + "#Glacier").version("2012-06-01") + .addTrait(ServiceTrait.builder().sdkId(sdkId).arnNamespace("glacier") + .cloudFormationName("Glacier").cloudTrailEventSource("glacier.amazonaws.com").build()); + for (OperationShape op : operations) { + b.addOperation(op.getId()); + } + return b.build(); + } + + // A raw streaming-payload request: input struct with an @httpPayload member targeting a + // @streaming blob (matches Glacier's UploadArchive/UploadMultipartPart body member). + private static StructureShape streamingInput(String name) { + return StructureShape.builder().id(NS + "#" + name) + .addMember(MemberShape.builder().id(NS + "#" + name + "$body") + .target(NS + "#Stream").addTrait(new HttpPayloadTrait()).build()) + .build(); + } + + private static StructureShape plainInput(String name) { + return StructureShape.builder().id(NS + "#" + name) + .addMember(MemberShape.builder().id(NS + "#" + name + "$vaultName") + .target("smithy.api#String").build()) + .build(); + } + + // A paginateable request: input struct with an integer @httpQuery `limit` (page-size) member, + // matching the type Coral2Smithy's GlacierTransformer produces upstream. + private static StructureShape queryLimitInput(String name) { + return StructureShape.builder().id(NS + "#" + name) + .addMember(MemberShape.builder().id(NS + "#" + name + "$limit") + .target(NS + "#intType").addTrait(new HttpQueryTrait("limit")).build()) + .build(); + } + + private static OperationShape op(String name, StructureShape input) { + return OperationShape.builder().id(NS + "#" + name).input(input.getId()).build(); + } + + private static Model modelWith(ServiceShape svc, software.amazon.smithy.model.shapes.Shape... shapes) { + Model.Builder b = Model.builder().addShape(svc) + .addShape(BlobShape.builder().id(NS + "#Stream").addTrait(new StreamingTrait()).build()) + // The service string shape the limit retype retargets to (siblings like marker use it). + .addShape(StringShape.builder().id(NS + "#string").build()) + .addShape(IntegerShape.builder().id(NS + "#intType").build()) + .addShape(StringShape.builder().id("smithy.api#String").build()); + for (software.amazon.smithy.model.shapes.Shape s : shapes) { + b.addShape(s); + } + return b.build(); + } + + @Test + void noOpForOtherService() { + ServiceShape svc = ServiceShape.builder().id("com.amazonaws.other#Other").version("1") + .addTrait(ServiceTrait.builder().sdkId("Other").arnNamespace("other") + .cloudFormationName("Other").cloudTrailEventSource("other").build()).build(); + Model m = Model.builder().addShape(svc).build(); + Model out = GlacierTransforms.asTransform().apply(m, svc); + assertSame(m, out, "non-glacier service must be untouched"); + } + + @Test + void stampsVersionHeaderOnStreamingRequestInputs() { + StructureShape upload = streamingInput("UploadArchiveInput"); + OperationShape uploadOp = op("UploadArchive", upload); + ServiceShape svc = glacierService("Glacier", uploadOp); + Model out = GlacierTransforms.asTransform().apply(modelWith(svc, upload, uploadOp), svc); + + AdditionalRequestHeadersTrait trait = out + .expectShape(ShapeId.from(NS + "#UploadArchiveInput"), StructureShape.class) + .getTrait(AdditionalRequestHeadersTrait.class) + .orElseThrow(() -> new AssertionError("streaming request input must carry the trait")); + assertEquals(1, trait.getHeaders().size()); + assertEquals("2012-06-01", trait.getHeaders().get("x-amz-glacier-version"), + "header value is the service API version"); + } + + @Test + void doesNotStampNonStreamingRequestInputs() { + StructureShape plain = plainInput("CompleteVaultLockInput"); + OperationShape plainOp = op("CompleteVaultLock", plain); + ServiceShape svc = glacierService("Glacier", plainOp); + Model out = GlacierTransforms.asTransform().apply(modelWith(svc, plain, plainOp), svc); + + assertFalse(out.expectShape(ShapeId.from(NS + "#CompleteVaultLockInput"), StructureShape.class) + .hasTrait(AdditionalRequestHeadersTrait.class), + "non-streaming request input must not carry the trait"); + } + + @Test + void retypesQueryLimitMemberBackToString() { + StructureShape listJobs = queryLimitInput("ListJobsInput"); + OperationShape listJobsOp = op("ListJobs", listJobs); + ServiceShape svc = glacierService("Glacier", listJobsOp); + Model out = GlacierTransforms.asTransform().apply(modelWith(svc, listJobs, listJobsOp), svc); + + MemberShape limit = out.expectShape(ShapeId.from(NS + "#ListJobsInput"), StructureShape.class) + .getMember("limit").orElseThrow(); + assertEquals(NS + "#string", limit.getTarget().toString(), + "query limit member retargeted to the service string shape"); + assertTrue(out.expectShape(limit.getTarget()).isStringShape(), "target is a string shape"); + } + + @Test + void leavesQueryLimitUnchangedWhenAlreadyString() { + StructureShape listJobs = StructureShape.builder().id(NS + "#ListJobsInput") + .addMember(MemberShape.builder().id(NS + "#ListJobsInput$limit") + .target(NS + "#string").addTrait(new HttpQueryTrait("limit")).build()) + .build(); + OperationShape listJobsOp = op("ListJobs", listJobs); + ServiceShape svc = glacierService("Glacier", listJobsOp); + Model out = GlacierTransforms.asTransform().apply(modelWith(svc, listJobs, listJobsOp), svc); + + MemberShape limit = out.expectShape(ShapeId.from(NS + "#ListJobsInput"), StructureShape.class) + .getMember("limit").orElseThrow(); + assertEquals(NS + "#string", limit.getTarget().toString(), "already-string limit is untouched"); + } + + @Test + void isIdempotent() { + StructureShape upload = streamingInput("UploadArchiveInput"); + OperationShape uploadOp = op("UploadArchive", upload); + ServiceShape svc = glacierService("Glacier", uploadOp); + Model once = GlacierTransforms.asTransform().apply(modelWith(svc, upload, uploadOp), svc); + Model twice = GlacierTransforms.asTransform().apply(once, svc); + + assertTrue(twice.expectShape(ShapeId.from(NS + "#UploadArchiveInput"), StructureShape.class) + .hasTrait(AdditionalRequestHeadersTrait.class), + "re-applying keeps a single trait without error"); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java index f16b7f325c8..86f54fbb4de 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupportTest.java @@ -7,6 +7,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import org.junit.jupiter.api.Test; import software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait; +import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; @@ -178,6 +179,45 @@ void appendEnumValues_nonIdentifierMemberName_throws() { () -> TransformSupport.appendEnumValues(shape, map("us-east-1", "us-east-1"))); } + @Test + void appendEnumValuesByName_appendsToNamedEnum() { + EnumShape state = EnumShape.builder().id("com.example#SpotInstanceState") + .addMember("open", "open").build(); + Model out = TransformSupport.appendEnumValuesByName( + Model.builder().addShape(state).build(), "SpotInstanceState", List.of("disabled")); + EnumShape e = out.expectShape(state.getId()).asEnumShape().orElseThrow(); + assertTrue(e.getEnumValues().values().contains("disabled"), "value appended"); + assertTrue(e.getEnumValues().values().contains("open"), "existing value preserved"); + } + + @Test + void appendEnumValuesByName_shapeAbsent_returnsSameModel() { + EnumShape other = EnumShape.builder().id("com.example#Other").addMember("a", "a").build(); + Model in = Model.builder().addShape(other).build(); + assertSame(in, TransformSupport.appendEnumValuesByName(in, "Missing", List.of("x")), + "absent enum: the model is returned unchanged"); + } + + @Test + void appendEnumValuesByName_allValuesPresent_returnsSameModel() { + EnumShape state = EnumShape.builder().id("com.example#S").addMember("a", "a").build(); + Model in = Model.builder().addShape(state).build(); + assertSame(in, TransformSupport.appendEnumValuesByName(in, "S", List.of("a")), + "idempotent: no new values means the model is returned unchanged"); + } + + @Test + void appendEnumEntriesByName_appendsHyphenatedRegionValue() { + EnumShape region = EnumShape.builder().id("com.example#BucketLocationConstraint") + .addMember("us_west_2", "us-west-2").build(); + Model out = TransformSupport.appendEnumEntriesByName( + Model.builder().addShape(region).build(), "BucketLocationConstraint", + map("us_east_1", "us-east-1")); + EnumShape e = out.expectShape(region.getId()).asEnumShape().orElseThrow(); + assertTrue(e.getEnumValues().values().contains("us-east-1"), "hyphenated value appended"); + assertTrue(e.getAllMembers().containsKey("us_east_1"), "identifier-safe member name"); + } + @Test void renameMember_existingJsonName_isNotOverridden() { StructureShape s = StructureShape.builder().id("com.example#Req") From e5348394df20fa6aed1688ff6b305e160d755473 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 1 Sep 2026 17:04:46 -0400 Subject: [PATCH 35/44] Smithy: emit request DumpBodyToUrl override protocol-agnostically --- .../generators/model/ModelCodegenPlugin.java | 4 +- .../model/protocol/JsonProtocolTraits.java | 9 ++ .../protocol/QueryXmlProtocolTraits.java | 15 +-- .../model/renderers/RequestRenderer.java | 15 +++ .../transforms/SupportsPresigningTrait.java | 27 ++++++ .../SupportsPresigningTransform.java | 58 ++++++++++++ .../ProtocolTraitsCharacterizationTest.java | 12 ++- .../generators/model/RequestRendererTest.java | 37 ++++++++ .../protocol/JsonProtocolTraitsTest.java | 25 +++++ .../model/protocol/XmlProtocolTraitsTest.java | 14 +-- .../SupportsPresigningTransformTest.java | 92 +++++++++++++++++++ 11 files changed, 286 insertions(+), 22 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index 0079380c411..6714c7a0dd9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -18,6 +18,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.S3Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SourceRegionTransform; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SqsTransforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTransform; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; import software.amazon.smithy.model.Model; @@ -64,7 +65,8 @@ public void execute(PluginContext context) { DynamoDbTransforms.asTransform(), S3Transforms.asTransform(), S3ControlTransforms.asTransform(), - GlacierTransforms.asTransform() + GlacierTransforms.asTransform(), + SupportsPresigningTransform.asTransform() )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java index fac78e66949..a8e2370b0c4 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java @@ -6,6 +6,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -212,5 +213,13 @@ public void writeRequestMethodImpls(CppWriter writer, String className, writer.write(""); writeAddQueryStringParametersImpl(writer, className, shape, model); } + // A presignable request (e.g. Polly's SynthesizeSpeech) declares the protocol-agnostic + // DumpBodyToUrl override (emitted by RequestRenderer); the real body defers with serde, as + // SerializePayload does, so this is a stub. UnreferencedParam.h is in serdeIncludes(REQUEST_SOURCE). + if (shape.hasTrait(SupportsPresigningTrait.class)) { + writer.write(""); + writer.write("void $L::DumpBodyToUrl(Aws::Http::URI& uri) const { AWS_UNREFERENCED_PARAM(uri); }", + className); + } } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java index 2d6be30a89c..84c9baf6906 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java @@ -216,18 +216,9 @@ public void writeRequestMethodDecls(CppWriter writer, String exportMacro, writer.write(""); writeAddQueryStringParametersDecl(writer, exportMacro); } - writer.write(""); - // DumpBodyToUrl is a protected virtual in AmazonWebServiceRequest, so the override - // is bracketed under protected: and the section restored to public: afterwards, - // matching the legacy C2J layout. - writer.dedent(); - writer.write("protected:"); - writer.indent(); - writer.write("$L void DumpBodyToUrl(Aws::Http::URI& uri) const override;", exportMacro); - writer.dedent(); - writer.write(""); - writer.write("public:"); - writer.indent(); + // The protected DumpBodyToUrl override declaration is emitted protocol-agnostically by + // RequestRenderer (gated on SupportsPresigningTrait), which all query/ec2 requests carry; + // only the impl below is protocol-specific. } @Override diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index 1189ffffa7e..84680e5fbeb 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -15,6 +15,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.RequestInfo; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.OverrideStreamingTrait; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.Emit; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.SmithyEndpointsJmesPathVisitor; import software.amazon.smithy.jmespath.JmespathExpression; @@ -150,6 +151,20 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.write("$L std::shared_ptr GetBody() const override;", ctx.exportMacro()); } ctx.protocolTraits().writeRequestMethodDecls(writer, ctx.exportMacro(), shape, operation, ctx.model()); + // DumpBodyToUrl is emitted protocol-agnostically (C2J RequestHeader.vm gates it only on + // $shape.supportsPresigning). It is a protected virtual in AmazonWebServiceRequest, so the + // override is bracketed under protected: and the section restored to public: afterwards. + if (shape.hasTrait(SupportsPresigningTrait.class)) { + writer.write(""); + writer.dedent(); + writer.write("protected:"); + writer.indent(); + writer.write("$L void DumpBodyToUrl(Aws::Http::URI& uri) const override;", ctx.exportMacro()); + writer.dedent(); + writer.write(""); + writer.write("public:"); + writer.indent(); + } // Request-feature methods driven by operation traits, in C2J RequestHeader.vm order: // @httpChecksum, @httpChecksumRequired (legacy Content-MD5), then @requestCompression. renderChecksumDecls(writer, shape, operation); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java new file mode 100644 index 00000000000..8dd4b6eaa04 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java @@ -0,0 +1,27 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link SupportsPresigningTransform} + * on each request structure that C2J's generators flag with {@code shape.setSupportsPresigning(true)} + * (every query/ec2 request via {@code QueryCppClientGenerator}, plus Polly's {@code SynthesizeSpeech}). + * C2J's shared {@code RequestHeader.vm} emits the protected {@code DumpBodyToUrl(Aws::Http::URI&)} + * override under {@code #if($shape.supportsPresigning())}, independent of protocol; request rendering + * turns this marker into that same protected override so the declaration stays protocol-agnostic + * while each protocol supplies only the method body. Kept as a marker + generic renderer rule (not a + * service-name {@code if}) so the renderer stays service-agnostic. + */ +public final class SupportsPresigningTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#supportsPresigning"); + + public SupportsPresigningTrait() { + super(ID, Node.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java new file mode 100644 index 00000000000..2b048b687cf --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java @@ -0,0 +1,58 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.UnitTypeTrait; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stamps the internal {@link SupportsPresigningTrait} onto the request structures that C2J flags + * with {@code shape.setSupportsPresigning(true)}, so the protocol-agnostic {@code DumpBodyToUrl} + * override is emitted by request rendering. C2J's {@code QueryCppClientGenerator} sets the flag on + * every query/ec2 request; Polly additionally sets it on {@code SynthesizeSpeech}. No-op for any + * other service, leaving the model instance untouched. + */ +public final class SupportsPresigningTransform { + + private SupportsPresigningTransform() {} + + public static ModelTransform asTransform() { + return SupportsPresigningTransform::apply; + } + + private static Model apply(Model model, ServiceShape service) { + Protocol protocol = ProtocolResolver.resolve(service, model); + boolean queryLike = protocol == Protocol.QUERY_XML || protocol == Protocol.EC2; + boolean polly = "polly".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + if (!queryLike && !polly) { + return model; + } + List updated = new ArrayList<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + boolean target = queryLike + ? !op.getInputShape().equals(UnitTypeTrait.UNIT) + : "SynthesizeSpeech".equals(op.getId().getName()); + if (target) { + model.getShape(op.getInputShape()).flatMap(Shape::asStructureShape) + .filter(s -> !s.hasTrait(SupportsPresigningTrait.class)) + .ifPresent(s -> updated.add( + s.toBuilder().addTrait(new SupportsPresigningTrait()).build())); + } + } + return updated.isEmpty() ? model + : model.toBuilder().addShapes(updated.toArray(new Shape[0])).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java index 2936fe29d35..921e61d5e2b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java @@ -74,7 +74,7 @@ private static Model modelFor(Protocol p) { .build(); // Input carries a plain member, an httpHeader member, and an httpQuery member so // both request Axis-1 predicates (header + query) fire. - StructureShape input = StructureShape.builder() + StructureShape.Builder inputBuilder = StructureShape.builder() .id("com.example#DoThingInput") .addMember("name", str.getId()) .addMember("nested", nested.getId()) @@ -83,8 +83,14 @@ private static Model modelFor(Protocol p) { .addTrait(new software.amazon.smithy.model.traits.HttpHeaderTrait("X-Thing")).build()) .addMember(software.amazon.smithy.model.shapes.MemberShape.builder() .id("com.example#DoThingInput$q").target(str.getId()) - .addTrait(new software.amazon.smithy.model.traits.HttpQueryTrait("q")).build()) - .build(); + .addTrait(new software.amazon.smithy.model.traits.HttpQueryTrait("q")).build()); + // SupportsPresigningTransform stamps every query/ec2 request; mirror it in the fixture so this + // end-to-end characterization pins the same post-transform output (protected DumpBodyToUrl decl). + if (p == Protocol.QUERY_XML || p == Protocol.EC2) { + inputBuilder.addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model + .transforms.SupportsPresigningTrait()); + } + StructureShape input = inputBuilder.build(); // Output carries a plain member and an httpResponseCode member. StructureShape output = StructureShape.builder() .id("com.example#DoThingOutput") diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index 23127963a4e..556066faf67 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -9,6 +9,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.RenderContext; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.OverrideStreamingTrait; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait; import org.junit.jupiter.api.Test; import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.model.Model; @@ -142,6 +143,42 @@ void withoutOverrideStreamingTrait_omitsIsStreaming() { "unmarked requests must not emit IsStreaming: " + h); } + private static Model supportsPresigningModel(boolean marked) { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape.Builder inB = StructureShape.builder() + .id("com.example#DoThingRequest").addMember("name", str.getId()); + if (marked) { + inB.addTrait(new SupportsPresigningTrait()); + } + StructureShape input = inB.build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); + OperationShape op = OperationShape.builder().id("com.example#DoThing") + .input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder().id("com.example#Example") + .version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, input, output, op, service).build(); + } + + @Test + void supportsPresigningTrait_emitsProtectedDumpBodyToUrlOverride() { + // C2J's RequestHeader.vm emits DumpBodyToUrl protocol-agnostically under + // #if($shape.supportsPresigning()); the marker drives the same protected override here. + String h = renderDoThingRequestHeader(supportsPresigningModel(true)); + assertTrue(h.contains("protected:"), "presignable request must open a protected: section: " + h); + assertTrue(h.contains( + "AWS_EXAMPLE_API void DumpBodyToUrl(Aws::Http::URI& uri) const override;"), + "presignable request must declare the DumpBodyToUrl override: " + h); + assertTrue(h.contains("public:"), "presignable request must restore public: afterwards: " + h); + } + + @Test + void withoutSupportsPresigningTrait_omitsDumpBodyToUrl() { + String h = renderDoThingRequestHeader(supportsPresigningModel(false)); + assertFalse(h.contains("DumpBodyToUrl"), + "unmarked requests must not declare DumpBodyToUrl: " + h); + } + @Test void streamingResponseRequest_hasEventStreamAugmentation() { // Model: operation with streaming OUTPUT only (like SubscribeToShard / ConverseStream) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java index 1a1957d5568..daad7f4a1f2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java @@ -239,6 +239,31 @@ void restJson_queryMember_isWireSerialized() { assertTrue(i.contains("uri.AddQueryStringParameter(\"q\", ss.str());"), i); } + @Test + void supportsPresigning_emitsDumpBodyToUrlStubImpl() { + // A presignable request (Polly SynthesizeSpeech) carries SupportsPresigningTrait; the decl is + // emitted by RequestRenderer, and JsonProtocolTraits supplies a stub impl that defers serde. + var req = reqWith(false, false).toBuilder() + .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms + .SupportsPresigningTrait()) + .build(); + var model = modelWith(req); + String i = render(w -> restJson.writeRequestMethodImpls( + w, "SynthesizeSpeechRequest", req, opDoThing(), svcAthena(), model)); + assertTrue(i.contains( + "void SynthesizeSpeechRequest::DumpBodyToUrl(Aws::Http::URI& uri) const { AWS_UNREFERENCED_PARAM(uri); }"), + i); + } + + @Test + void withoutSupportsPresigning_omitsDumpBodyToUrlImpl() { + var req = reqWith(false, false); var model = modelWith(req); + String i = render(w -> restJson.writeRequestMethodImpls( + w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertFalse(i.contains("DumpBodyToUrl"), + "non-presignable request must not emit a DumpBodyToUrl impl: " + i); + } + @Test void payloadStubs_areProtocolAgnostic() { String event = render(w -> json.writeEventPayloadDecode(w, "ShardEvent", "m_onShardEvent")); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java index 31336b33e23..ea9d11d1241 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java @@ -272,14 +272,15 @@ void restXml_withoutEmbeddedErrorsTrait_omitsHasEmbeddedError() { // ---------- Query/EC2 request contract (Axis-1 gating + protected DumpBodyToUrl) ---------- @Test - void queryXml_serializePayloadAndProtectedDumpBodyToUrl() { + void queryXml_serializePayloadAndDumpBodyToUrlImpl() { + // The DumpBodyToUrl DECL is now emitted protocol-agnostically by RequestRenderer (gated on + // SupportsPresigningTrait), so the query traits' decls no longer carry it; only the IMPL is here. var req = reqWith(false, false); var model = modelWith(req); ProtocolTraits q = new QueryXmlProtocolTraits(Protocol.QUERY_XML); String d = renderInClassBody(w -> q.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThing(), model)); assertTrue(d.contains("Aws::String SerializePayload() const override;"), d); - assertTrue(d.contains("void DumpBodyToUrl(Aws::Http::URI& uri) const override;"), d); - assertTrue(d.contains("protected:"), d); - assertTrue(d.contains("public:"), d); + assertFalse(d.contains("DumpBodyToUrl"), + "DumpBodyToUrl decl moved to RequestRenderer; query traits must not emit it: " + d); assertFalse(d.contains("GetRequestSpecificHeaders"), d); String i = render(w -> q.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); assertTrue(i.contains("Aws::String DoThingRequest::SerializePayload() const { return {}; }"), i); @@ -287,12 +288,13 @@ void queryXml_serializePayloadAndProtectedDumpBodyToUrl() { } @Test - void queryXml_withHeaderMember_alsoEmitsHeaders_andStillDumpBodyToUrl() { + void queryXml_withHeaderMember_alsoEmitsHeaders() { var req = reqWith(true, false); var model = modelWith(req); ProtocolTraits q = new QueryXmlProtocolTraits(Protocol.QUERY_XML); String d = renderInClassBody(w -> q.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThing(), model)); assertTrue(d.contains("GetRequestSpecificHeaders() const override;"), d); - assertTrue(d.contains("DumpBodyToUrl"), d); + assertFalse(d.contains("DumpBodyToUrl"), + "DumpBodyToUrl decl moved to RequestRenderer; query traits must not emit it: " + d); } // ---------- Query/EC2 result ResponseMetadata / requestId extraction ---------- diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java new file mode 100644 index 00000000000..239b957d0ad --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java @@ -0,0 +1,92 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait; +import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait; +import software.amazon.smithy.aws.traits.protocols.RestJson1Trait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.Trait; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SupportsPresigningTransformTest { + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + private static boolean stamped(Model m, String requestShapeName) { + return m.expectShape(ShapeId.from("com.example#" + requestShapeName), StructureShape.class) + .hasTrait(SupportsPresigningTrait.class); + } + + /** Two operations (each with a distinct input) under a service carrying {@code protocolTrait}. */ + private static Model twoOpModel(Trait protocolTrait, ServiceTrait serviceTrait, + String opAName, String opBName) { + StructureShape inA = StructureShape.builder().id("com.example#" + opAName + "Request").build(); + StructureShape inB = StructureShape.builder().id("com.example#" + opBName + "Request").build(); + OperationShape opA = OperationShape.builder() + .id("com.example#" + opAName).input(inA.getId()).build(); + OperationShape opB = OperationShape.builder() + .id("com.example#" + opBName).input(inB.getId()).build(); + ServiceShape.Builder svc = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(protocolTrait) + .addOperation(opA.getId()).addOperation(opB.getId()); + if (serviceTrait != null) { + svc.addTrait(serviceTrait); + } + return Model.assembler().addShapes(inA, inB, opA, opB, svc.build()).assemble().unwrap(); + } + + private static ServiceTrait pollyServiceTrait() { + return ServiceTrait.builder().sdkId("polly").arnNamespace("polly") + .cloudFormationName("Polly").cloudTrailEventSource("polly").build(); + } + + @Test + void queryXmlService_stampsEveryOperationInput() { + Model m = twoOpModel(new AwsQueryTrait(), null, "GetUser", "CreateUser"); + Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "GetUserRequest"), "query input must be stamped"); + assertTrue(stamped(out, "CreateUserRequest"), "query input must be stamped"); + } + + @Test + void ec2Service_stampsEveryOperationInput() { + Model m = twoOpModel(new Ec2QueryTrait(), null, "DescribeThings", "RunThings"); + Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "DescribeThingsRequest"), "ec2 input must be stamped"); + assertTrue(stamped(out, "RunThingsRequest"), "ec2 input must be stamped"); + } + + @Test + void pollyService_stampsOnlySynthesizeSpeechInput() { + Model m = twoOpModel(RestJson1Trait.builder().build(), pollyServiceTrait(), + "SynthesizeSpeech", "DescribeVoices"); + Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "SynthesizeSpeechRequest"), "Polly SynthesizeSpeech must be stamped"); + assertFalse(stamped(out, "DescribeVoicesRequest"), + "Polly must stamp only SynthesizeSpeech, not other operations"); + } + + @Test + void plainRestJsonService_stampsNothing() { + Model m = twoOpModel(RestJson1Trait.builder().build(), null, "GetThing", "PutThing"); + Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + assertSame(m, out, "a non-query, non-Polly rest-json service must be left untouched"); + assertFalse(stamped(out, "GetThingRequest")); + assertFalse(stamped(out, "PutThingRequest")); + } +} From 7e148a50a0cd21825ce56e353b67054c31dc71fc Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 1 Sep 2026 17:11:10 -0400 Subject: [PATCH 36/44] Smithy: ignore @deprecated operations so orphaned request/result structs are not emitted --- .../generators/model/ShapeClassifier.java | 7 +-- .../model/transforms/GlobalTransforms.java | 21 ++++++- .../model/GlobalTransformsTest.java | 61 +++++++++++++++++++ .../generators/model/ShapeClassifierTest.java | 49 +++++++++++++++ 4 files changed, 133 insertions(+), 5 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java index 353b2cbce3e..ecbba4994d8 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java @@ -8,7 +8,6 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.CustomRenderedTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; @@ -105,7 +104,6 @@ private ShapeClassifier() {} * @return classified shapes grouped by generation bucket */ public static ClassifiedShapes classify(Model model, ServiceShape service, Protocol protocol) { - TopDownIndex index = TopDownIndex.of(model); Set reachable = GlobalTransforms.computeReachableShapes(model, service); Set inputShapeIds = new HashSet<>(); @@ -122,8 +120,9 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto List outgoingEventStreams = new ArrayList<>(); List blobPayloadEvents = new ArrayList<>(); - // Collect operation inputs/outputs and identify event stream handlers - for (OperationShape op : index.getContainedOperations(service)) { + // Collect operation inputs/outputs and identify event stream handlers. Deprecated operations + // are excluded (matching legacy C2J), so their orphaned request/result structs never emit. + for (OperationShape op : GlobalTransforms.nonDeprecatedOperations(model, service)) { // Use getInputShape() (not getInput()) so no-input operations, whose input // target is smithy.api#Unit, still produce a RequestInfo. C2J emits a Request // class for every operation; the generated client method references it. diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java index 19f27ff1965..2fac9fcc43c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java @@ -140,7 +140,7 @@ private static List> reservedRenames(StructureShape st public static Set computeReachableShapes(Model model, ServiceShape service) { Walker walker = new Walker(model); Set reachable = new HashSet<>(); - for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + for (OperationShape op : nonDeprecatedOperations(model, service)) { addReachableFrom(op.getInputShape(), walker, model, reachable); op.getOutput().ifPresent(id -> addReachableFrom(id, walker, model, reachable)); op.getErrors().forEach(id -> addReachableFrom(id, walker, model, reachable)); @@ -153,6 +153,25 @@ private static void addReachableFrom(ShapeId root, Walker walker, Model model, S model.getShape(root).ifPresent(shape -> out.addAll(walker.walkShapeIds(shape))); } + /** + * Returns the service's operations excluding any marked {@code @deprecated}. Legacy C2J drops + * deprecated operations entirely (they never appear in the generated client), so their input and + * output structures — orphaned once the operation is gone — are not emitted either. This is the + * single filter used by every emission-driving iteration over the service's operations + * ({@link #computeReachableShapes} and {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier#classify}) + * so reachability and classification stay in agreement. A structure still referenced by a live + * operation remains reachable through that operation, so shared structures are unaffected. + * + * @param model the Smithy model + * @param service the service whose operations are being generated + * @return the service's non-deprecated operations + */ + public static List nonDeprecatedOperations(Model model, ServiceShape service) { + return TopDownIndex.of(model).getContainedOperations(service).stream() + .filter(op -> !op.hasTrait(DeprecatedTrait.class)) + .collect(Collectors.toList()); + } + /** * Returns this class as a ModelTransform. * diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index 727706c24aa..777388f8cfe 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java @@ -447,6 +447,67 @@ void computeReachableShapes_errorShapes_areReachable() { assertTrue(reachable.contains(ShapeId.from("com.example#MyError"))); } + @Test + void computeReachableShapes_excludesStructReachableOnlyViaDeprecatedOperation() { + // A @deprecated operation is dropped entirely (matching legacy C2J, which omits deprecated + // operations). A struct reachable ONLY through the deprecated op's input must fall out of the + // reachable set, while a struct shared with a live op stays reachable via that live op. + StructureShape deprecatedOnly = StructureShape.builder() + .id("com.example#DeprecatedOnly") + .addMember(MemberShape.builder() + .id("com.example#DeprecatedOnly$x").target("smithy.api#String").build()) + .build(); + StructureShape shared = StructureShape.builder() + .id("com.example#SharedDetail") + .addMember(MemberShape.builder() + .id("com.example#SharedDetail$y").target("smithy.api#String").build()) + .build(); + StructureShape deprecatedInput = StructureShape.builder() + .id("com.example#DeprecatedInput") + .addMember(MemberShape.builder() + .id("com.example#DeprecatedInput$only").target(deprecatedOnly.getId()).build()) + .addMember(MemberShape.builder() + .id("com.example#DeprecatedInput$shared").target(shared.getId()).build()) + .build(); + StructureShape deprecatedOutput = StructureShape.builder() + .id("com.example#DeprecatedOutput").build(); + StructureShape liveInput = StructureShape.builder() + .id("com.example#LiveInput") + .addMember(MemberShape.builder() + .id("com.example#LiveInput$shared").target(shared.getId()).build()) + .build(); + StructureShape liveOutput = StructureShape.builder() + .id("com.example#LiveOutput").build(); + OperationShape deprecatedOp = OperationShape.builder() + .id("com.example#DeprecatedOp") + .input(deprecatedInput.getId()).output(deprecatedOutput.getId()) + .addTrait(software.amazon.smithy.model.traits.DeprecatedTrait.builder().build()) + .build(); + OperationShape liveOp = OperationShape.builder() + .id("com.example#LiveOp") + .input(liveInput.getId()).output(liveOutput.getId()) + .build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#MyService").version("2024-01-01") + .addOperation(deprecatedOp.getId()).addOperation(liveOp.getId()) + .build(); + Model model = Model.assembler() + .addShapes(deprecatedOnly, shared, deprecatedInput, deprecatedOutput, + liveInput, liveOutput, deprecatedOp, liveOp, service) + .assemble().unwrap(); + + Set reachable = GlobalTransforms.computeReachableShapes(model, service); + + assertFalse(reachable.contains(ShapeId.from("com.example#DeprecatedOnly")), + "struct reachable only via a @deprecated operation must be excluded"); + assertFalse(reachable.contains(ShapeId.from("com.example#DeprecatedInput")), + "the input of a @deprecated operation must be excluded"); + assertTrue(reachable.contains(ShapeId.from("com.example#SharedDetail")), + "struct shared with a live operation must remain reachable"); + assertTrue(reachable.contains(ShapeId.from("com.example#LiveInput")), + "the input of a live operation must remain reachable"); + } + // --- dropDeprecatedMembers tests --- @Test diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java index 3ae0b5d93c2..42517bc1492 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java @@ -679,6 +679,55 @@ void classifyDropsIncomingEventStreamUnionFromSubObjects() { "incoming event-stream union dropped from subObjects: " + classified.subObjects()); } + @Test + void deprecatedOperation_inputAndOutputExcludedFromRequestsAndResults() { + // Legacy C2J drops @deprecated operations entirely, so their orphaned request/result structs + // are never emitted. The classifier must not put a @deprecated op's input in requests nor its + // output in results, while a live op's input/output are present. + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape deprecatedRequest = StructureShape.builder() + .id("com.example#DeprecatedRequest").addMember("id", str.getId()).build(); + StructureShape deprecatedResponse = StructureShape.builder() + .id("com.example#DeprecatedResponse").addMember("r", str.getId()).build(); + StructureShape liveRequest = StructureShape.builder() + .id("com.example#LiveRequest").addMember("id", str.getId()).build(); + StructureShape liveResponse = StructureShape.builder() + .id("com.example#LiveResponse").addMember("r", str.getId()).build(); + OperationShape deprecatedOp = OperationShape.builder() + .id("com.example#DeprecatedOp") + .input(deprecatedRequest.getId()).output(deprecatedResponse.getId()) + .addTrait(DeprecatedTrait.builder().build()) + .build(); + OperationShape liveOp = OperationShape.builder() + .id("com.example#LiveOp") + .input(liveRequest.getId()).output(liveResponse.getId()) + .build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2023-01-01") + .addOperation(deprecatedOp.getId()).addOperation(liveOp.getId()) + .addTrait(ServiceTrait.builder().sdkId("test").arnNamespace("test") + .cloudFormationName("Test").cloudTrailEventSource("test").build()) + .build(); + Model model = Model.builder() + .addShapes(str, deprecatedRequest, deprecatedResponse, liveRequest, liveResponse, + deprecatedOp, liveOp, service) + .build(); + var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); + + assertTrue(classified.requests().stream() + .noneMatch(r -> r.shape().getId().getName().equals("DeprecatedRequest")), + "deprecated op input must not be a request: " + classified.requests()); + assertTrue(classified.results().stream() + .noneMatch(r -> r.shape().getId().getName().equals("DeprecatedResponse")), + "deprecated op output must not be a result: " + classified.results()); + assertTrue(classified.requests().stream() + .anyMatch(r -> r.shape().getId().getName().equals("LiveRequest")), + "live op input must be a request: " + classified.requests()); + assertTrue(classified.results().stream() + .anyMatch(r -> r.shape().getId().getName().equals("LiveResponse")), + "live op output must be a result: " + classified.results()); + } + @Test void classifiesEnumShape() { // StringShape with @enum trait -> classified as enum From 5bdb74fb9917dfe80225aee799c178f7583bcf5d Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 00:17:51 -0400 Subject: [PATCH 37/44] Smithy: injectResponseMetadata skips @deprecated operation outputs --- .../model/transforms/GlobalTransforms.java | 3 +- .../model/GlobalTransformsTest.java | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java index 2fac9fcc43c..a37787ebd3a 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java @@ -259,9 +259,8 @@ public static Model injectResponseMetadata(Model model, ServiceShape service) { List replacements = new ArrayList<>(); replacements.add(responseMetadata); - TopDownIndex index = TopDownIndex.of(model); Set outputIds = new HashSet<>(); - for (OperationShape op : index.getContainedOperations(service)) { + for (OperationShape op : nonDeprecatedOperations(model, service)) { op.getOutput().ifPresent(outputIds::add); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index 777388f8cfe..1198a76430d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java @@ -789,6 +789,54 @@ void injectResponseMetadata_awsJsonWithQueryCompatible_addsResponseMetadataStruc "ResponseMetadata should have a RequestId member"); } + @Test + void injectResponseMetadata_skipsDeprecatedOperationOutputSharedByLiveOp() { + // A @deprecated operation's output that is ALSO reused as a nested member by a live op's + // output is reachable/emitted as a sub-object, but C2J drops the deprecated op entirely and + // never injects ResponseMetadata into that shape. Only genuine live-op outputs get it. + StructureShape sharedOutput = StructureShape.builder() + .id("com.example#SharedOutput") + .addMember(MemberShape.builder() + .id("com.example#SharedOutput$value").target("smithy.api#String").build()) + .build(); + StructureShape liveOutput = StructureShape.builder() + .id("com.example#LiveOutput") + .addMember(MemberShape.builder() + .id("com.example#LiveOutput$nested").target(sharedOutput.getId()).build()) + .build(); + StructureShape deprecatedInput = StructureShape.builder() + .id("com.example#DeprecatedInput").build(); + StructureShape liveInput = StructureShape.builder() + .id("com.example#LiveInput").build(); + OperationShape deprecatedOp = OperationShape.builder() + .id("com.example#DeprecatedOp") + .input(deprecatedInput.getId()).output(sharedOutput.getId()) + .addTrait(software.amazon.smithy.model.traits.DeprecatedTrait.builder().build()) + .build(); + OperationShape liveOp = OperationShape.builder() + .id("com.example#LiveOp") + .input(liveInput.getId()).output(liveOutput.getId()) + .build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#Example").version("2024-01-01") + .addTrait(new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait()) + .addOperation(deprecatedOp.getId()).addOperation(liveOp.getId()) + .build(); + Model model = Model.assembler().addShapes(sharedOutput, liveOutput, deprecatedInput, + liveInput, deprecatedOp, liveOp, service).assemble().unwrap(); + + Model out = GlobalTransforms.injectResponseMetadata(model, serviceOf(model, "Example")); + + StructureShape sharedAfter = out.expectShape( + ShapeId.from("com.example#SharedOutput"), StructureShape.class); + assertFalse(sharedAfter.getMember("ResponseMetadata").isPresent(), + "a @deprecated op's output (only reused as a nested member) must not gain ResponseMetadata"); + StructureShape liveAfter = out.expectShape( + ShapeId.from("com.example#LiveOutput"), StructureShape.class); + assertTrue(liveAfter.getMember("ResponseMetadata").isPresent(), + "a genuine live-op output must still gain ResponseMetadata"); + } + @Test void injectResponseMetadata_awsJsonWithoutQueryCompatible_leavesResultUnchanged() { // Plain awsJson1_0 (no @awsQueryCompatible) must NOT get ResponseMetadata injected. From 29aa6bfea9b4d2f5b91ba019eb47b7ab8f0ecb0f Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 00:22:21 -0400 Subject: [PATCH 38/44] Smithy: RequestRenderer emits SignBody override for @unsignedPayload operations --- .../model/renderers/RequestRenderer.java | 16 ++++++ .../generators/model/RequestRendererTest.java | 52 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index 84680e5fbeb..e64117d50c9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -24,6 +24,7 @@ import software.amazon.smithy.model.node.NodeVisitor; import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.aws.traits.HttpChecksumTrait; +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.StructureShape; @@ -169,6 +170,7 @@ private void renderHeader(CppWriterDelegator writerDelegator, // @httpChecksum, @httpChecksumRequired (legacy Content-MD5), then @requestCompression. renderChecksumDecls(writer, shape, operation); renderContentMd5Decl(writer, operation); + renderSignBodyDecl(writer, shape, operation); renderRequestCompressionDecl(writer, operation); // S3 flips a couple of streaming-base requests back to non-streaming (C2J @@ -501,6 +503,20 @@ private void renderContentMd5Decl(CppWriter writer, OperationShape operation) { } } + /** + * Declares the inline {@code SignBody} override for an operation carrying + * {@code aws.auth#unsignedPayload} whose request has at least one member. C2J's + * RequestHeader.vm emits this for a {@code v4-unsigned-body} request + * ({@code #if(!$shape.signBody && $shape.members.size() > 0)}); the Smithy equivalent is the + * {@code @unsignedPayload} operation trait. This closes {@code Model::}-namespace parity only: + * the Smithy runtime does not consume {@code SignBody()} (it hardcodes signing). + */ + private void renderSignBodyDecl(CppWriter writer, StructureShape shape, OperationShape operation) { + if (operation.hasTrait(UnsignedPayloadTrait.class) && !shape.getAllMembers().isEmpty()) { + writer.write("$L bool SignBody() const override { return false; }", ctx.exportMacro()); + } + } + /** The C++ enum type name of a checksum algorithm/validation-mode member named by @httpChecksum. */ private String checksumMemberEnumType(StructureShape shape, OperationShape operation, String memberName) { MemberShape member = shape.getAllMembers().get(memberName); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index 556066faf67..f70ac806d0d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -947,4 +947,56 @@ void operationContextParams_multiSelectFlattenPattern_endToEnd() { "Missing result push for " + branch + ": " + c); } } + + // --- aws.auth#unsignedPayload (SignBody) --- + + /** + * Operation carrying {@code aws.auth#unsignedPayload}. When {@code emptyInput} is false the input + * has a member; when true the input has none (exercises the {@code !members.isEmpty()} guard). + * When {@code marked} is false the trait is omitted. + */ + private static Model unsignedPayloadModel(boolean marked, boolean emptyInput) { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape.Builder inB = StructureShape.builder().id("com.example#DoThingRequest"); + if (!emptyInput) { + inB.addMember("name", str.getId()); + } + StructureShape input = inB.build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); + OperationShape.Builder opB = OperationShape.builder().id("com.example#DoThing") + .input(input.getId()).output(output.getId()); + if (marked) { + opB.addTrait(new software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait()); + } + OperationShape op = opB.build(); + ServiceShape service = ServiceShape.builder().id("com.example#Example") + .version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, input, output, op, service).build(); + } + + @Test + void unsignedPayloadTrait_emitsSignBodyFalse() { + // C2J RequestHeader.vm: a v4-unsigned-body request with members emits SignBody() -> false. + // In Smithy that maps to the operation carrying aws.auth#unsignedPayload. + String h = renderDoThingRequestHeader(unsignedPayloadModel(true, false)); + assertTrue(h.contains("bool SignBody() const override { return false; }"), + "@unsignedPayload op with members must emit SignBody() -> false: " + h); + } + + @Test + void withoutUnsignedPayloadTrait_omitsSignBody() { + String h = renderDoThingRequestHeader(unsignedPayloadModel(false, false)); + assertFalse(h.contains("SignBody"), + "op without @unsignedPayload must not emit SignBody: " + h); + } + + @Test + void unsignedPayloadTraitWithEmptyRequest_omitsSignBody() { + // The !members.isEmpty() guard: an @unsignedPayload op whose request has no members + // must not emit SignBody (matches C2J's $shape.members.size() > 0 gate). + String h = renderDoThingRequestHeader(unsignedPayloadModel(true, true)); + assertFalse(h.contains("SignBody"), + "@unsignedPayload op with an empty request must not emit SignBody: " + h); + } } From 0710a06709954da98e5d4b104fd96561a2811246 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 00:31:09 -0400 Subject: [PATCH 39/44] Smithy: emit request IsChunked() override for chunked-encoding operations --- .../generators/model/ModelCodegenPlugin.java | 4 +- .../model/renderers/RequestRenderer.java | 9 ++ .../transforms/ChunkedEncodingTrait.java | 28 +++++ .../transforms/ChunkedEncodingTransform.java | 61 ++++++++++ .../generators/model/RequestRendererTest.java | 35 ++++++ .../ChunkedEncodingTransformTest.java | 110 ++++++++++++++++++ 6 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index 6714c7a0dd9..bb55352e32e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -9,6 +9,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.AccessAnalyzerTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ApiGatewayV2Transforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ChunkedEncodingTransform; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.DynamoDbTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.Ec2Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlacierTransforms; @@ -66,7 +67,8 @@ public void execute(PluginContext context) { S3Transforms.asTransform(), S3ControlTransforms.asTransform(), GlacierTransforms.asTransform(), - SupportsPresigningTransform.asTransform() + SupportsPresigningTransform.asTransform(), + ChunkedEncodingTransform.asTransform() )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index e64117d50c9..6a23bbebdd5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -14,6 +14,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.RequestInfo; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ChunkedEncodingTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.OverrideStreamingTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.Emit; @@ -171,6 +172,14 @@ private void renderHeader(CppWriterDelegator writerDelegator, renderChecksumDecls(writer, shape, operation); renderContentMd5Decl(writer, operation); renderSignBodyDecl(writer, shape, operation); + + // Chunked-encoding requests emit IsChunked() -> true right after SignBody and before + // IsStreaming (C2J RequestHeader.vm order); the marker is stamped by + // ChunkedEncodingTransform. + if (shape.hasTrait(ChunkedEncodingTrait.class)) { + writer.write("$L bool IsChunked() const override { return true; }", ctx.exportMacro()); + } + renderRequestCompressionDecl(writer, operation); // S3 flips a couple of streaming-base requests back to non-streaming (C2J diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java new file mode 100644 index 00000000000..db7744e5e2b --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java @@ -0,0 +1,28 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link ChunkedEncodingTransform} on + * each request structure for which C2J's {@code RequestHeader.vm} emits + * {@code bool IsChunked() const override { return true; }}. C2J gates that override on + * {@code ($metadata.serviceId=="MediaStore Data" || $operation.supportsChunkedEncoding)} together + * with {@code $shape.hasStreamMembers() && !$shape.signBody && $shape.members.size() > 0}; S3 sets + * {@code supportsChunkedEncoding} on {@code WriteGetObjectResponse} only. The transform collapses + * that emit-time condition into a single stamping decision so request rendering only has to turn the + * marker into the override. Kept as a marker + generic renderer rule (not a service-name {@code if}) + * so the renderer stays service-agnostic. + */ +public final class ChunkedEncodingTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#chunkedEncoding"); + + public ChunkedEncodingTrait() { + super(ID, Node.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java new file mode 100644 index 00000000000..2529fcc6dff --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java @@ -0,0 +1,61 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier; +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stamps the internal {@link ChunkedEncodingTrait} onto the request structures for which C2J's + * {@code RequestHeader.vm} emits {@code bool IsChunked() const override { return true; }}. C2J gates + * that override on {@code ($metadata.serviceId=="MediaStore Data" || $operation.supportsChunkedEncoding)} + * (S3 sets {@code supportsChunkedEncoding} on {@code WriteGetObjectResponse} only) combined with + * {@code $shape.hasStreamMembers() && !$shape.signBody && $shape.members.size() > 0}. This transform + * collapses that emit-time condition into a single stamping decision: an operation qualifies when it + * carries {@code aws.auth#unsignedPayload} (the {@code !signBody} proxy), its input is a raw + * streaming payload request (the {@code hasStreamMembers} proxy, which also guarantees members > 0), + * and either the service is MediaStore Data or the operation is S3's {@code WriteGetObjectResponse}. + * No-op for any other service/operation, leaving the model instance untouched. + */ +public final class ChunkedEncodingTransform { + + private static final String WRITE_GET_OBJECT_RESPONSE = "WriteGetObjectResponse"; + + private ChunkedEncodingTransform() {} + + public static ModelTransform asTransform() { + return ChunkedEncodingTransform::apply; + } + + private static Model apply(Model model, ServiceShape service) { + boolean mediaStoreData = + "mediastore-data".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + List updated = new ArrayList<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + boolean supportsChunkedEncoding = + mediaStoreData || WRITE_GET_OBJECT_RESPONSE.equals(op.getId().getName()); + if (supportsChunkedEncoding + && op.hasTrait(UnsignedPayloadTrait.class) + && ShapeClassifier.isRawStreamingPayloadRequest(op, model)) { + model.getShape(op.getInputShape()).flatMap(Shape::asStructureShape) + .filter(s -> !s.hasTrait(ChunkedEncodingTrait.class)) + .ifPresent(s -> updated.add( + s.toBuilder().addTrait(new ChunkedEncodingTrait()).build())); + } + } + return updated.isEmpty() ? model + : model.toBuilder().addShapes(updated.toArray(new Shape[0])).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index f70ac806d0d..6abe568d27c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -999,4 +999,39 @@ void unsignedPayloadTraitWithEmptyRequest_omitsSignBody() { assertFalse(h.contains("SignBody"), "@unsignedPayload op with an empty request must not emit SignBody: " + h); } + + // --- aws.cpp.internal#chunkedEncoding (IsChunked) --- + + private static Model chunkedEncodingModel(boolean marked) { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape.Builder inB = StructureShape.builder() + .id("com.example#DoThingRequest").addMember("name", str.getId()); + if (marked) { + inB.addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ChunkedEncodingTrait()); + } + StructureShape input = inB.build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); + OperationShape op = OperationShape.builder().id("com.example#DoThing") + .input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder().id("com.example#Example") + .version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, input, output, op, service).build(); + } + + @Test + void chunkedEncodingTrait_emitsIsChunkedTrue() { + // C2J RequestHeader.vm emits IsChunked() -> true for a chunked-encoding request; the marker + // (stamped by ChunkedEncodingTransform) drives the same override here. + String h = renderDoThingRequestHeader(chunkedEncodingModel(true)); + assertTrue(h.contains("bool IsChunked() const override { return true; }"), + "ChunkedEncodingTrait must emit the IsChunked override: " + h); + } + + @Test + void withoutChunkedEncodingTrait_omitsIsChunked() { + String h = renderDoThingRequestHeader(chunkedEncodingModel(false)); + assertFalse(h.contains("IsChunked"), + "unmarked requests must not emit IsChunked: " + h); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java new file mode 100644 index 00000000000..bc3515b8060 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java @@ -0,0 +1,110 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.HttpPayloadTrait; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ChunkedEncodingTransformTest { + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + private static boolean stamped(Model m, String requestShapeName) { + return m.expectShape(ShapeId.from("com.example#" + requestShapeName), StructureShape.class) + .hasTrait(ChunkedEncodingTrait.class); + } + + private static ServiceTrait serviceTrait(String sdkId) { + return ServiceTrait.builder().sdkId(sdkId).arnNamespace("ns") + .cloudFormationName("Cfn").cloudTrailEventSource("src").build(); + } + + /** + * Builds a single-operation service. {@code streaming} adds a raw {@code @httpPayload} blob body + * (plus a plain member so members > 0); {@code unsigned} adds {@code aws.auth#unsignedPayload}. + */ + private static Model oneOpModel(String sdkId, String opName, boolean streaming, boolean unsigned) { + StringShape str = StringShape.builder().id("com.example#String").build(); + BlobShape blob = BlobShape.builder().id("com.example#Body").build(); + StructureShape.Builder inB = StructureShape.builder().id("com.example#" + opName + "Request") + .addMember("name", str.getId()); + if (streaming) { + inB.addMember(MemberShape.builder() + .id("com.example#" + opName + "Request$body").target(blob.getId()) + .addTrait(new HttpPayloadTrait()).build()); + } + StructureShape input = inB.build(); + OperationShape.Builder opB = OperationShape.builder() + .id("com.example#" + opName).input(input.getId()); + if (unsigned) { + opB.addTrait(new UnsignedPayloadTrait()); + } + OperationShape op = opB.build(); + ServiceShape service = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(serviceTrait(sdkId)) + .addOperation(op.getId()).build(); + return Model.assembler().addShapes(str, blob, input, op, service).assemble().unwrap(); + } + + @Test + void mediaStoreDataUnsignedStreamingOp_stampsInput() { + Model m = oneOpModel("MediaStore Data", "PutObject", true, true); + Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "PutObjectRequest"), + "MediaStore Data unsigned-payload streaming request must be stamped"); + } + + @Test + void mediaStoreDataNonStreamingOp_notStamped() { + Model m = oneOpModel("MediaStore Data", "DescribeObject", false, true); + Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + assertSame(m, out, "no qualifying operation must leave the model untouched"); + assertFalse(stamped(out, "DescribeObjectRequest"), + "a non-streaming request must not be stamped"); + } + + @Test + void mediaStoreDataSignedStreamingOp_notStamped() { + Model m = oneOpModel("MediaStore Data", "PutObject", true, false); + Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + assertSame(m, out, "no qualifying operation must leave the model untouched"); + assertFalse(stamped(out, "PutObjectRequest"), + "a signed (no @unsignedPayload) request must not be stamped"); + } + + @Test + void s3WriteGetObjectResponseUnsignedStreamingOp_stampsInput() { + Model m = oneOpModel("S3", "WriteGetObjectResponse", true, true); + Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "WriteGetObjectResponseRequest"), + "S3 WriteGetObjectResponse unsigned-payload streaming request must be stamped"); + } + + @Test + void unrelatedServiceStreamingOp_notStamped() { + Model m = oneOpModel("S3", "PutObject", true, true); + Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + assertSame(m, out, "an unrelated operation must leave the model untouched"); + assertFalse(stamped(out, "PutObjectRequest"), + "only WriteGetObjectResponse (or MediaStore Data) may be stamped"); + } +} From 939dcd9c62eb281d9268b9170e3aba5555980281 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 00:38:51 -0400 Subject: [PATCH 40/44] Smithy: emit request IsLongPollingOperation() override for long-polling operations --- .../generators/model/ModelCodegenPlugin.java | 4 +- .../model/renderers/RequestRenderer.java | 7 ++ .../model/transforms/LongPollingTrait.java | 29 +++++ .../transforms/LongPollingTransform.java | 63 +++++++++++ .../generators/model/RequestRendererTest.java | 51 +++++++++ .../transforms/LongPollingTransformTest.java | 102 ++++++++++++++++++ 6 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index bb55352e32e..1bf8ecf3831 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -15,6 +15,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlacierTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LambdaTransforms; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LongPollingTransform; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.S3ControlTransforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.S3Transforms; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SourceRegionTransform; @@ -68,7 +69,8 @@ public void execute(PluginContext context) { S3ControlTransforms.asTransform(), GlacierTransforms.asTransform(), SupportsPresigningTransform.asTransform(), - ChunkedEncodingTransform.asTransform() + ChunkedEncodingTransform.asTransform(), + LongPollingTransform.asTransform() )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index 6a23bbebdd5..0f37b41a3f5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -15,6 +15,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.RequestInfo; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.ChunkedEncodingTrait; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LongPollingTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.OverrideStreamingTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.endpointcontext.Emit; @@ -141,6 +142,12 @@ private void renderHeader(CppWriterDelegator writerDelegator, if (streamingRequest) { writer.write("inline virtual bool IsEventStreamRequest() const override { return true; }"); } + // Long-polling requests emit IsLongPollingOperation() -> true with the top identity + // methods (C2J RequestHeader.vm order: after IsEventStreamRequest, before + // HasEventStreamResponse); the marker is stamped by LongPollingTransform. + if (shape.hasTrait(LongPollingTrait.class)) { + writer.write("inline virtual bool IsLongPollingOperation() const override { return true; }"); + } if (streamingResponse) { writer.write("inline virtual bool HasEventStreamResponse() const override { return true; }"); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java new file mode 100644 index 00000000000..d213ad92c68 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java @@ -0,0 +1,29 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Internal marker (never declared in any model file) placed by {@link LongPollingTransform} on the + * request structure of each operation for which C2J's {@code RequestHeader.vm} emits + * {@code bool IsLongPollingOperation() const override { return true; }} (gated on + * {@code $operation.longPolling}). C2J sets that flag from + * {@code C2jModelToGeneratorModelTransformer.LONG_POLLING_OPERATIONS}, a hardcoded per-serviceId set + * ({@code SQS: [ReceiveMessage]}, {@code SFN: [GetActivityTask]}, + * {@code SWF: [PollForActivityTask, PollForDecisionTask]}). The transform collapses that lookup into a + * single stamping decision so request rendering only has to turn the marker into the override. Kept as + * a marker + generic renderer rule (not a service-name {@code if}) so the renderer stays + * service-agnostic. + */ +public final class LongPollingTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cpp.internal#longPolling"); + + public LongPollingTrait() { + super(ID, Node.objectNode()); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java new file mode 100644 index 00000000000..6ad636277bc --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java @@ -0,0 +1,63 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ModelTransform; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Stamps the internal {@link LongPollingTrait} onto the request structures of the long-polling + * operations that C2J flags with {@code operation.setLongPolling(true)}, so the + * {@code IsLongPollingOperation() -> true} override is emitted by request rendering. C2J's + * {@code C2jModelToGeneratorModelTransformer.LONG_POLLING_OPERATIONS} keys the set on the C2J + * {@code serviceId} ({@code SQS}, {@code SFN}, {@code SWF}); the Smithy equivalent is the RAW smithy + * service name (the lowercased/hyphenated sdkId from + * {@link ServiceNameUtil#getSmithyServiceName(ServiceShape, Map)} with a {@code null} service map, so + * no c2jMap remap such as {@code sfn->states} is applied). No-op for any other service, leaving the + * model instance untouched. + */ +public final class LongPollingTransform { + + private static final Map> LONG_POLLING_OPERATIONS = Map.of( + "sqs", Set.of("ReceiveMessage"), + "sfn", Set.of("GetActivityTask"), + "swf", Set.of("PollForActivityTask", "PollForDecisionTask") + ); + + private LongPollingTransform() {} + + public static ModelTransform asTransform() { + return LongPollingTransform::apply; + } + + private static Model apply(Model model, ServiceShape service) { + Set longPollOps = + LONG_POLLING_OPERATIONS.get(ServiceNameUtil.getSmithyServiceName(service, null)); + if (longPollOps == null) { + return model; + } + List updated = new ArrayList<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + if (longPollOps.contains(op.getId().getName())) { + model.getShape(op.getInputShape()).flatMap(Shape::asStructureShape) + .filter(s -> !s.hasTrait(LongPollingTrait.class)) + .ifPresent(s -> updated.add( + s.toBuilder().addTrait(new LongPollingTrait()).build())); + } + } + return updated.isEmpty() ? model + : model.toBuilder().addShapes(updated.toArray(new Shape[0])).build(); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index 6abe568d27c..30c5df9d600 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -1034,4 +1034,55 @@ void withoutChunkedEncodingTrait_omitsIsChunked() { assertFalse(h.contains("IsChunked"), "unmarked requests must not emit IsChunked: " + h); } + + // --- aws.cpp.internal#longPolling (IsLongPollingOperation) --- + + private static Model longPollingModel(boolean marked) { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape.Builder inB = StructureShape.builder() + .id("com.example#DoThingRequest").addMember("name", str.getId()); + if (marked) { + inB.addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.LongPollingTrait()); + } + StructureShape input = inB.build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); + OperationShape op = OperationShape.builder().id("com.example#DoThing") + .input(input.getId()).output(output.getId()).build(); + ServiceShape service = ServiceShape.builder().id("com.example#Example") + .version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, input, output, op, service).build(); + } + + @Test + void longPollingTrait_emitsIsLongPollingOperationTrue() { + // C2J RequestHeader.vm emits IsLongPollingOperation() -> true for a long-polling request + // (gated on $operation.longPolling); the marker (stamped by LongPollingTransform) drives the + // same override here. + String h = renderDoThingRequestHeader(longPollingModel(true)); + assertTrue(h.contains("bool IsLongPollingOperation() const override { return true; }"), + "LongPollingTrait must emit the IsLongPollingOperation override: " + h); + } + + @Test + void longPollingTrait_emittedWithTopIdentityMethods() { + // Ordering: IsLongPollingOperation sits with the top identity methods (after + // GetServiceRequestName, before SerializePayload), NOT down with the SignBody/IsChunked + // block. Matches C2J RequestHeader.vm lines 57-64. + String h = renderDoThingRequestHeader(longPollingModel(true)); + int requestName = h.indexOf("GetServiceRequestName"); + int longPolling = h.indexOf("IsLongPollingOperation"); + int serializePayload = h.indexOf("SerializePayload"); + assertTrue(requestName >= 0 && longPolling > requestName, + "IsLongPollingOperation must come after GetServiceRequestName: " + h); + assertTrue(serializePayload >= 0 && longPolling < serializePayload, + "IsLongPollingOperation must come before SerializePayload (top identity block): " + h); + } + + @Test + void withoutLongPollingTrait_omitsIsLongPollingOperation() { + String h = renderDoThingRequestHeader(longPollingModel(false)); + assertFalse(h.contains("IsLongPollingOperation"), + "unmarked requests must not emit IsLongPollingOperation: " + h); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java new file mode 100644 index 00000000000..6a96b92c588 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java @@ -0,0 +1,102 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LongPollingTransformTest { + + private static ServiceShape service(Model m) { + return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + } + + private static boolean stamped(Model m, String requestShapeName) { + return m.expectShape(ShapeId.from("com.example#" + requestShapeName), StructureShape.class) + .hasTrait(LongPollingTrait.class); + } + + private static ServiceTrait serviceTrait(String sdkId) { + return ServiceTrait.builder().sdkId(sdkId).arnNamespace("ns") + .cloudFormationName("Cfn").cloudTrailEventSource("src").build(); + } + + /** Builds a service with the given sdkId containing one input+operation per op name. */ + private static Model model(String sdkId, String... opNames) { + StringShape str = StringShape.builder().id("com.example#String").build(); + List shapes = new ArrayList<>(); + shapes.add(str); + ServiceShape.Builder serviceB = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(serviceTrait(sdkId)); + for (String opName : opNames) { + StructureShape input = StructureShape.builder() + .id("com.example#" + opName + "Request").addMember("name", str.getId()).build(); + OperationShape op = OperationShape.builder() + .id("com.example#" + opName).input(input.getId()).build(); + shapes.add(input); + shapes.add(op); + serviceB.addOperation(op.getId()); + } + shapes.add(serviceB.build()); + return Model.assembler() + .addShapes(shapes.toArray(new software.amazon.smithy.model.shapes.Shape[0])) + .assemble().unwrap(); + } + + @Test + void sqsReceiveMessage_stampsOnlyReceiveMessageInput() { + Model m = model("SQS", "ReceiveMessage", "SendMessage"); + Model out = LongPollingTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "ReceiveMessageRequest"), + "SQS ReceiveMessage input must be stamped"); + assertFalse(stamped(out, "SendMessageRequest"), + "a non-long-polling SQS operation must not be stamped"); + } + + @Test + void swf_stampsBothPollOperations() { + Model m = model("SWF", "PollForActivityTask", "PollForDecisionTask", "StartWorkflowExecution"); + Model out = LongPollingTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "PollForActivityTaskRequest"), + "SWF PollForActivityTask input must be stamped"); + assertTrue(stamped(out, "PollForDecisionTaskRequest"), + "SWF PollForDecisionTask input must be stamped"); + assertFalse(stamped(out, "StartWorkflowExecutionRequest"), + "a non-long-polling SWF operation must not be stamped"); + } + + @Test + void sfnGetActivityTask_stampsInput() { + Model m = model("SFN", "GetActivityTask", "StartExecution"); + Model out = LongPollingTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "GetActivityTaskRequest"), + "SFN GetActivityTask input must be stamped"); + assertFalse(stamped(out, "StartExecutionRequest"), + "a non-long-polling SFN operation must not be stamped"); + } + + @Test + void unrelatedService_stampsNothing() { + Model m = model("DynamoDB", "GetItem", "ReceiveMessage"); + Model out = LongPollingTransform.asTransform().apply(m, service(m)); + assertSame(m, out, "an unrelated service must leave the model untouched"); + assertFalse(stamped(out, "ReceiveMessageRequest"), + "an operation on an unrelated service must not be stamped even if its name matches"); + } +} From 043b7d581754e79307d64bfe2dfb1648bf9fe447 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 13:26:45 -0400 Subject: [PATCH 41/44] Smithy: stamp SupportsPresigning on operation to keep DumpBodyToUrl decl/impl symmetric --- .../model/protocol/JsonProtocolTraits.java | 9 +-- .../protocol/QueryXmlProtocolTraits.java | 15 +++-- .../model/renderers/RequestRenderer.java | 4 +- .../SupportsPresigningTransform.java | 25 ++++----- .../ProtocolTraitsCharacterizationTest.java | 19 ++++--- .../generators/model/RequestRendererTest.java | 47 +++++++++++++--- .../protocol/JsonProtocolTraitsTest.java | 19 ++++--- .../model/protocol/XmlProtocolTraitsTest.java | 27 +++++++-- .../SupportsPresigningTransformTest.java | 56 ++++++++++++++----- 9 files changed, 157 insertions(+), 64 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java index a8e2370b0c4..02cbe833222 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java @@ -213,10 +213,11 @@ public void writeRequestMethodImpls(CppWriter writer, String className, writer.write(""); writeAddQueryStringParametersImpl(writer, className, shape, model); } - // A presignable request (e.g. Polly's SynthesizeSpeech) declares the protocol-agnostic - // DumpBodyToUrl override (emitted by RequestRenderer); the real body defers with serde, as - // SerializePayload does, so this is a stub. UnreferencedParam.h is in serdeIncludes(REQUEST_SOURCE). - if (shape.hasTrait(SupportsPresigningTrait.class)) { + // A presignable operation (e.g. Polly's SynthesizeSpeech) declares the protocol-agnostic + // DumpBodyToUrl override (emitted by RequestRenderer, gated on the same operation trait); the + // real body defers with serde, as SerializePayload does, so this is a stub. + // UnreferencedParam.h is in serdeIncludes(REQUEST_SOURCE). + if (operation.hasTrait(SupportsPresigningTrait.class)) { writer.write(""); writer.write("void $L::DumpBodyToUrl(Aws::Http::URI& uri) const { AWS_UNREFERENCED_PARAM(uri); }", className); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java index 84c9baf6906..19c923cb95b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java @@ -6,6 +6,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -217,8 +218,8 @@ public void writeRequestMethodDecls(CppWriter writer, String exportMacro, writeAddQueryStringParametersDecl(writer, exportMacro); } // The protected DumpBodyToUrl override declaration is emitted protocol-agnostically by - // RequestRenderer (gated on SupportsPresigningTrait), which all query/ec2 requests carry; - // only the impl below is protocol-specific. + // RequestRenderer (gated on the operation's SupportsPresigningTrait, stamped on every + // query/ec2 operation including Unit-input ops); only the impl below is protocol-specific. } @Override @@ -236,8 +237,12 @@ public void writeRequestMethodImpls(CppWriter writer, String className, writer.write(""); writeAddQueryStringParametersImpl(writer, className, shape, model); } - writer.write(""); - writer.write("void $L::DumpBodyToUrl(Aws::Http::URI& uri) const { uri.SetQueryString(SerializePayload()); }", - className); + // Gate the DumpBodyToUrl impl on the same operation trait as the RequestRenderer decl so the + // two stay symmetric: a Unit-input op with the trait gets both, an op without it gets neither. + if (operation.hasTrait(SupportsPresigningTrait.class)) { + writer.write(""); + writer.write("void $L::DumpBodyToUrl(Aws::Http::URI& uri) const { uri.SetQueryString(SerializePayload()); }", + className); + } } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index 0f37b41a3f5..f6c96708703 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -163,7 +163,9 @@ private void renderHeader(CppWriterDelegator writerDelegator, // DumpBodyToUrl is emitted protocol-agnostically (C2J RequestHeader.vm gates it only on // $shape.supportsPresigning). It is a protected virtual in AmazonWebServiceRequest, so the // override is bracketed under protected: and the section restored to public: afterwards. - if (shape.hasTrait(SupportsPresigningTrait.class)) { + // The trait is stamped on the OPERATION (SupportsPresigningTransform) so Unit-input ops + // are covered and the decl stays symmetric with the protocol-emitted impl. + if (operation.hasTrait(SupportsPresigningTrait.class)) { writer.write(""); writer.dedent(); writer.write("protected:"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java index 2b048b687cf..d61c53db417 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java @@ -13,16 +13,18 @@ import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.traits.UnitTypeTrait; import java.util.ArrayList; import java.util.List; /** - * Stamps the internal {@link SupportsPresigningTrait} onto the request structures that C2J flags - * with {@code shape.setSupportsPresigning(true)}, so the protocol-agnostic {@code DumpBodyToUrl} - * override is emitted by request rendering. C2J's {@code QueryCppClientGenerator} sets the flag on - * every query/ec2 request; Polly additionally sets it on {@code SynthesizeSpeech}. No-op for any + * Stamps the internal {@link SupportsPresigningTrait} onto the OPERATIONS that C2J flags with + * {@code shape.setSupportsPresigning(true)}, so the protocol-agnostic {@code DumpBodyToUrl} override + * (declaration and impl) is emitted by request rendering. In C2J {@code supportsPresigning} is + * conceptually per-operation; the trait is stamped on the operation (never shared, unlike the + * {@code smithy.api#Unit} input) so it also covers {@code Unit}-input operations and keeps the decl + * and impl symmetric and protocol-agnostic. C2J's {@code QueryCppClientGenerator} sets the flag for + * every query/ec2 operation; Polly additionally sets it on {@code SynthesizeSpeech}. No-op for any * other service, leaving the model instance untouched. */ public final class SupportsPresigningTransform { @@ -42,14 +44,11 @@ private static Model apply(Model model, ServiceShape service) { } List updated = new ArrayList<>(); for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { - boolean target = queryLike - ? !op.getInputShape().equals(UnitTypeTrait.UNIT) - : "SynthesizeSpeech".equals(op.getId().getName()); - if (target) { - model.getShape(op.getInputShape()).flatMap(Shape::asStructureShape) - .filter(s -> !s.hasTrait(SupportsPresigningTrait.class)) - .ifPresent(s -> updated.add( - s.toBuilder().addTrait(new SupportsPresigningTrait()).build())); + // Operations are never Unit, so query/ec2 stamps every operation (covering Unit-input + // ops). Idempotent: skip operations that already carry the trait. + boolean target = queryLike || "SynthesizeSpeech".equals(op.getId().getName()); + if (target && !op.hasTrait(SupportsPresigningTrait.class)) { + updated.add(op.toBuilder().addTrait(new SupportsPresigningTrait()).build()); } } return updated.isEmpty() ? model diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java index 921e61d5e2b..e4b8e298ba1 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java @@ -84,12 +84,6 @@ private static Model modelFor(Protocol p) { .addMember(software.amazon.smithy.model.shapes.MemberShape.builder() .id("com.example#DoThingInput$q").target(str.getId()) .addTrait(new software.amazon.smithy.model.traits.HttpQueryTrait("q")).build()); - // SupportsPresigningTransform stamps every query/ec2 request; mirror it in the fixture so this - // end-to-end characterization pins the same post-transform output (protected DumpBodyToUrl decl). - if (p == Protocol.QUERY_XML || p == Protocol.EC2) { - inputBuilder.addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model - .transforms.SupportsPresigningTrait()); - } StructureShape input = inputBuilder.build(); // Output carries a plain member and an httpResponseCode member. StructureShape output = StructureShape.builder() @@ -99,11 +93,18 @@ private static Model modelFor(Protocol p) { .id("com.example#DoThingOutput$status").target(intShape.getId()) .addTrait(new software.amazon.smithy.model.traits.HttpResponseCodeTrait()).build()) .build(); - OperationShape op = OperationShape.builder() + // SupportsPresigningTransform stamps every query/ec2 OPERATION; mirror it in the fixture so + // this end-to-end characterization pins the same post-transform output (protected + // DumpBodyToUrl decl + protocol-specific impl). + OperationShape.Builder opBuilder = OperationShape.builder() .id("com.example#DoThing") .input(input.getId()) - .output(output.getId()) - .build(); + .output(output.getId()); + if (p == Protocol.QUERY_XML || p == Protocol.EC2) { + opBuilder.addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model + .transforms.SupportsPresigningTrait()); + } + OperationShape op = opBuilder.build(); ServiceShape service = ServiceShape.builder() .id("com.example#Example") .version("2024-01-01") diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index 30c5df9d600..10638077b1f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -145,25 +145,48 @@ void withoutOverrideStreamingTrait_omitsIsStreaming() { private static Model supportsPresigningModel(boolean marked) { StringShape str = StringShape.builder().id("com.example#String").build(); - StructureShape.Builder inB = StructureShape.builder() - .id("com.example#DoThingRequest").addMember("name", str.getId()); + StructureShape input = StructureShape.builder() + .id("com.example#DoThingRequest").addMember("name", str.getId()).build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); + // The trait is stamped on the OPERATION (SupportsPresigningTransform), so the decl keys off + // operation.hasTrait(...) — not the input shape. + OperationShape.Builder opB = OperationShape.builder().id("com.example#DoThing") + .input(input.getId()).output(output.getId()); if (marked) { - inB.addTrait(new SupportsPresigningTrait()); + opB.addTrait(new SupportsPresigningTrait()); } - StructureShape input = inB.build(); + OperationShape op = opB.build(); + ServiceShape service = ServiceShape.builder().id("com.example#Example") + .version("2024-01-01").addOperation(op.getId()).build(); + return Model.builder().addShapes(str, input, output, op, service).build(); + } + + /** + * A presignable operation whose input is the shared {@code smithy.api#Unit} (no {@code input(...)} + * set) — the case that broke: the input shape cannot carry the trait, but the operation can, so + * the decl must still be emitted. Mirrors an IAM-style query op like {@code GetAccountSummary}. + */ + private static Model supportsPresigningUnitInputModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape output = StructureShape.builder() .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); OperationShape op = OperationShape.builder().id("com.example#DoThing") - .input(input.getId()).output(output.getId()).build(); + .output(output.getId()) + .addTrait(new SupportsPresigningTrait()) + .build(); ServiceShape service = ServiceShape.builder().id("com.example#Example") .version("2024-01-01").addOperation(op.getId()).build(); - return Model.builder().addShapes(str, input, output, op, service).build(); + // Assemble (not Model.builder) so the smithy.api#Unit prelude shape exists: the operation has + // no input, so its input target defaults to Unit, which ShapeClassifier resolves to build the + // request. Unit must be present in the model for the request class to be emitted. + return Model.assembler().addShapes(str, output, op, service).assemble().unwrap(); } @Test void supportsPresigningTrait_emitsProtectedDumpBodyToUrlOverride() { // C2J's RequestHeader.vm emits DumpBodyToUrl protocol-agnostically under - // #if($shape.supportsPresigning()); the marker drives the same protected override here. + // #if($shape.supportsPresigning()); the operation trait drives the same protected override. String h = renderDoThingRequestHeader(supportsPresigningModel(true)); assertTrue(h.contains("protected:"), "presignable request must open a protected: section: " + h); assertTrue(h.contains( @@ -172,6 +195,16 @@ void supportsPresigningTrait_emitsProtectedDumpBodyToUrlOverride() { assertTrue(h.contains("public:"), "presignable request must restore public: afterwards: " + h); } + @Test + void supportsPresigningTrait_unitInputOperation_stillEmitsDumpBodyToUrl() { + // Regression: a Unit-input op stamps the operation (not the shared Unit input), and the decl + // must still be emitted so it stays symmetric with the protocol-emitted impl. + String h = renderDoThingRequestHeader(supportsPresigningUnitInputModel()); + assertTrue(h.contains( + "AWS_EXAMPLE_API void DumpBodyToUrl(Aws::Http::URI& uri) const override;"), + "Unit-input presignable operation must still declare DumpBodyToUrl: " + h); + } + @Test void withoutSupportsPresigningTrait_omitsDumpBodyToUrl() { String h = renderDoThingRequestHeader(supportsPresigningModel(false)); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java index daad7f4a1f2..0a436e66646 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java @@ -41,6 +41,12 @@ private static software.amazon.smithy.model.shapes.StructureShape reqWith(boolea private static software.amazon.smithy.model.shapes.OperationShape opDoThing() { return software.amazon.smithy.model.shapes.OperationShape.builder().id("com.example#DoThing").build(); } + /** A presignable operation: carries the internal trait stamped by SupportsPresigningTransform. */ + private static software.amazon.smithy.model.shapes.OperationShape opDoThingPresigning() { + return software.amazon.smithy.model.shapes.OperationShape.builder().id("com.example#DoThing") + .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait()) + .build(); + } private static software.amazon.smithy.model.shapes.ServiceShape svcAthena() { return software.amazon.smithy.model.shapes.ServiceShape.builder() .id("com.example#AmazonAthena").version("2017-05-18").build(); @@ -241,15 +247,12 @@ void restJson_queryMember_isWireSerialized() { @Test void supportsPresigning_emitsDumpBodyToUrlStubImpl() { - // A presignable request (Polly SynthesizeSpeech) carries SupportsPresigningTrait; the decl is - // emitted by RequestRenderer, and JsonProtocolTraits supplies a stub impl that defers serde. - var req = reqWith(false, false).toBuilder() - .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms - .SupportsPresigningTrait()) - .build(); - var model = modelWith(req); + // A presignable operation (Polly SynthesizeSpeech) carries SupportsPresigningTrait on the + // OPERATION; the decl is emitted by RequestRenderer, and JsonProtocolTraits supplies a stub + // impl (gated on the same operation trait) that defers serde. + var req = reqWith(false, false); var model = modelWith(req); String i = render(w -> restJson.writeRequestMethodImpls( - w, "SynthesizeSpeechRequest", req, opDoThing(), svcAthena(), model)); + w, "SynthesizeSpeechRequest", req, opDoThingPresigning(), svcAthena(), model)); assertTrue(i.contains( "void SynthesizeSpeechRequest::DumpBodyToUrl(Aws::Http::URI& uri) const { AWS_UNREFERENCED_PARAM(uri); }"), i); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java index ea9d11d1241..ee64dda0c18 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java @@ -58,6 +58,12 @@ private static software.amazon.smithy.model.shapes.StructureShape reqWith(boolea private static software.amazon.smithy.model.shapes.OperationShape opDoThing() { return software.amazon.smithy.model.shapes.OperationShape.builder().id("com.example#DoThing").build(); } + /** A presignable operation: carries the internal trait stamped by SupportsPresigningTransform. */ + private static software.amazon.smithy.model.shapes.OperationShape opDoThingPresigning() { + return software.amazon.smithy.model.shapes.OperationShape.builder().id("com.example#DoThing") + .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.SupportsPresigningTrait()) + .build(); + } private static software.amazon.smithy.model.shapes.ServiceShape svcAthena() { return software.amazon.smithy.model.shapes.ServiceShape.builder() .id("com.example#AmazonAthena").version("2017-05-18").build(); @@ -273,20 +279,33 @@ void restXml_withoutEmbeddedErrorsTrait_omitsHasEmbeddedError() { @Test void queryXml_serializePayloadAndDumpBodyToUrlImpl() { - // The DumpBodyToUrl DECL is now emitted protocol-agnostically by RequestRenderer (gated on - // SupportsPresigningTrait), so the query traits' decls no longer carry it; only the IMPL is here. + // The DumpBodyToUrl DECL is emitted protocol-agnostically by RequestRenderer (gated on the + // operation's SupportsPresigningTrait), so the query traits' decls no longer carry it; the + // IMPL is here and now gated on the SAME operation trait so decl+impl stay symmetric. var req = reqWith(false, false); var model = modelWith(req); ProtocolTraits q = new QueryXmlProtocolTraits(Protocol.QUERY_XML); - String d = renderInClassBody(w -> q.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThing(), model)); + String d = renderInClassBody(w -> q.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThingPresigning(), model)); assertTrue(d.contains("Aws::String SerializePayload() const override;"), d); assertFalse(d.contains("DumpBodyToUrl"), "DumpBodyToUrl decl moved to RequestRenderer; query traits must not emit it: " + d); assertFalse(d.contains("GetRequestSpecificHeaders"), d); - String i = render(w -> q.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + String i = render(w -> q.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThingPresigning(), svcAthena(), model)); assertTrue(i.contains("Aws::String DoThingRequest::SerializePayload() const { return {}; }"), i); assertTrue(i.contains("void DoThingRequest::DumpBodyToUrl(Aws::Http::URI& uri) const { uri.SetQueryString(SerializePayload()); }"), i); } + @Test + void queryXml_withoutPresigningOperation_omitsDumpBodyToUrlImpl() { + // Compile-break regression: the impl must be ABSENT when the operation lacks the trait, so a + // Unit-input op that never got the operation trait doesn't emit an impl with no declaration. + var req = reqWith(false, false); var model = modelWith(req); + ProtocolTraits q = new QueryXmlProtocolTraits(Protocol.QUERY_XML); + String i = render(w -> q.writeRequestMethodImpls(w, "DoThingRequest", req, opDoThing(), svcAthena(), model)); + assertTrue(i.contains("Aws::String DoThingRequest::SerializePayload() const { return {}; }"), i); + assertFalse(i.contains("DumpBodyToUrl"), + "operation without SupportsPresigningTrait must not emit the DumpBodyToUrl impl: " + i); + } + @Test void queryXml_withHeaderMember_alsoEmitsHeaders() { var req = reqWith(true, false); var model = modelWith(req); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java index 239b957d0ad..e138b7e54d5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java @@ -26,8 +26,9 @@ private static ServiceShape service(Model m) { return m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); } - private static boolean stamped(Model m, String requestShapeName) { - return m.expectShape(ShapeId.from("com.example#" + requestShapeName), StructureShape.class) + /** Whether the OPERATION (not its input) carries the internal trait after transformation. */ + private static boolean stamped(Model m, String operationName) { + return m.expectShape(ShapeId.from("com.example#" + operationName), OperationShape.class) .hasTrait(SupportsPresigningTrait.class); } @@ -50,34 +51,63 @@ private static Model twoOpModel(Trait protocolTrait, ServiceTrait serviceTrait, return Model.assembler().addShapes(inA, inB, opA, opB, svc.build()).assemble().unwrap(); } + /** + * One operation with a normal input and one operation with NO input (its input target defaults to + * {@code smithy.api#Unit}), under a service carrying {@code protocolTrait}. Mirrors an IAM-style + * {@code GetAccountSummary} where the request struct is the shared {@code Unit}. + */ + private static Model opPlusUnitInputModel(Trait protocolTrait, String normalOp, String unitOp) { + StructureShape in = StructureShape.builder().id("com.example#" + normalOp + "Request").build(); + OperationShape normal = OperationShape.builder() + .id("com.example#" + normalOp).input(in.getId()).build(); + OperationShape unit = OperationShape.builder() + .id("com.example#" + unitOp).build(); + ServiceShape svc = ServiceShape.builder() + .id("com.example#TestService").version("2024-01-01") + .addTrait(protocolTrait) + .addOperation(normal.getId()).addOperation(unit.getId()).build(); + return Model.assembler().addShapes(in, normal, unit, svc).assemble().unwrap(); + } + private static ServiceTrait pollyServiceTrait() { return ServiceTrait.builder().sdkId("polly").arnNamespace("polly") .cloudFormationName("Polly").cloudTrailEventSource("polly").build(); } @Test - void queryXmlService_stampsEveryOperationInput() { + void queryXmlService_stampsEveryOperation() { Model m = twoOpModel(new AwsQueryTrait(), null, "GetUser", "CreateUser"); Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); - assertTrue(stamped(out, "GetUserRequest"), "query input must be stamped"); - assertTrue(stamped(out, "CreateUserRequest"), "query input must be stamped"); + assertTrue(stamped(out, "GetUser"), "query operation must be stamped"); + assertTrue(stamped(out, "CreateUser"), "query operation must be stamped"); } @Test - void ec2Service_stampsEveryOperationInput() { + void ec2Service_stampsEveryOperation() { Model m = twoOpModel(new Ec2QueryTrait(), null, "DescribeThings", "RunThings"); Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); - assertTrue(stamped(out, "DescribeThingsRequest"), "ec2 input must be stamped"); - assertTrue(stamped(out, "RunThingsRequest"), "ec2 input must be stamped"); + assertTrue(stamped(out, "DescribeThings"), "ec2 operation must be stamped"); + assertTrue(stamped(out, "RunThings"), "ec2 operation must be stamped"); + } + + @Test + void queryXmlService_stampsUnitInputOperation() { + // The regression: a Unit-input query op (e.g. iam GetAccountSummary) cannot stamp the shared + // Unit input shape, but the operation itself carries the trait so decl+impl stay symmetric. + Model m = opPlusUnitInputModel(new AwsQueryTrait(), "ListUsers", "GetAccountSummary"); + Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + assertTrue(stamped(out, "ListUsers"), "normal-input query operation must be stamped"); + assertTrue(stamped(out, "GetAccountSummary"), + "Unit-input query operation must be stamped on the operation itself"); } @Test - void pollyService_stampsOnlySynthesizeSpeechInput() { + void pollyService_stampsOnlySynthesizeSpeechOperation() { Model m = twoOpModel(RestJson1Trait.builder().build(), pollyServiceTrait(), "SynthesizeSpeech", "DescribeVoices"); Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); - assertTrue(stamped(out, "SynthesizeSpeechRequest"), "Polly SynthesizeSpeech must be stamped"); - assertFalse(stamped(out, "DescribeVoicesRequest"), + assertTrue(stamped(out, "SynthesizeSpeech"), "Polly SynthesizeSpeech must be stamped"); + assertFalse(stamped(out, "DescribeVoices"), "Polly must stamp only SynthesizeSpeech, not other operations"); } @@ -86,7 +116,7 @@ void plainRestJsonService_stampsNothing() { Model m = twoOpModel(RestJson1Trait.builder().build(), null, "GetThing", "PutThing"); Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); assertSame(m, out, "a non-query, non-Polly rest-json service must be left untouched"); - assertFalse(stamped(out, "GetThingRequest")); - assertFalse(stamped(out, "PutThingRequest")); + assertFalse(stamped(out, "GetThing")); + assertFalse(stamped(out, "PutThing")); } } From b18ffe48e416cd1fdd4ffff5a4a902233d0783b0 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 14:27:12 -0400 Subject: [PATCH 42/44] Comment cleanup --- .../generators/model/CppTypeMapper.java | 52 +++---- .../generators/model/EnumRenderer.java | 5 - .../generators/model/MemberRenderer.java | 66 ++++----- .../generators/model/ModelCodegenPlugin.java | 5 - .../generators/model/ModelTransform.java | 8 +- .../generators/model/ProtocolResolver.java | 24 ++-- .../generators/model/RenderContext.java | 5 +- .../generators/model/ShapeClassifier.java | 64 ++++----- .../generators/model/ShapeRenderer.java | 11 +- .../model/protocol/CborProtocolTraits.java | 5 +- .../model/protocol/JsonProtocolTraits.java | 15 +- .../model/protocol/ProtocolTraits.java | 46 +++--- .../protocol/QueryXmlProtocolTraits.java | 16 +-- .../model/protocol/RestXmlProtocolTraits.java | 14 +- .../model/renderers/DynamoDbRenderer.java | 7 +- .../model/renderers/EventPayloadRenderer.java | 18 ++- .../model/renderers/EventStreamRenderer.java | 26 +--- .../model/renderers/IncludeSets.java | 23 ++- .../generators/model/renderers/ModelFile.java | 14 +- .../OutgoingEventStreamRenderer.java | 28 ++-- .../renderers/RequestHeaderSerializer.java | 45 +++--- .../renderers/RequestQuerySerializer.java | 61 ++++---- .../model/renderers/RequestRenderer.java | 136 ++++++++---------- .../model/renderers/ResultRenderer.java | 33 ++--- .../model/renderers/SubObjectRenderer.java | 36 ++--- .../SmithyEndpointsJmesPathVisitor.java | 15 +- .../transforms/AccessAnalyzerTransforms.java | 16 +-- .../AdditionalRequestHeadersTrait.java | 17 +-- .../model/transforms/ChecksumMemberTrait.java | 11 +- .../transforms/ChunkedEncodingTrait.java | 13 +- .../transforms/ChunkedEncodingTransform.java | 15 +- .../model/transforms/CustomRenderedTrait.java | 19 +-- .../CustomizedAccessLogTagTrait.java | 14 +- .../model/transforms/DynamoDbTransforms.java | 20 +-- .../model/transforms/Ec2Transforms.java | 41 ++---- .../model/transforms/EmbeddedErrorsTrait.java | 10 +- .../model/transforms/GlacierTransforms.java | 29 ++-- .../model/transforms/GlobalTransforms.java | 93 +++++------- .../model/transforms/LongPollingTrait.java | 15 +- .../transforms/LongPollingTransform.java | 14 +- .../transforms/OverrideStreamingTrait.java | 12 +- .../model/transforms/S3ControlTransforms.java | 14 +- .../model/transforms/S3Transforms.java | 85 +++++------ .../transforms/SourceRegionTransform.java | 8 +- .../transforms/SupportsPresigningTrait.java | 12 +- .../SupportsPresigningTransform.java | 16 +-- .../model/transforms/TopLevelHostIdTrait.java | 10 +- .../model/transforms/TransformSupport.java | 100 +++++-------- .../generators/model/CppTypeMapperTest.java | 27 ++-- .../generators/model/EnumRendererTest.java | 5 +- .../model/EventPayloadRendererTest.java | 4 +- .../model/EventStreamRendererTest.java | 13 +- .../model/GlobalTransformsTest.java | 31 ++-- .../model/MemberRendererOutputTest.java | 16 +-- .../generators/model/MemberRendererTest.java | 24 ++-- .../generators/model/ModelGeneratorTest.java | 51 +++---- .../OutgoingEventStreamRendererTest.java | 6 +- .../ProtocolTraitsCharacterizationTest.java | 27 ++-- .../generators/model/RequestRendererTest.java | 106 +++++--------- .../generators/model/ResultRendererTest.java | 8 +- .../generators/model/ServiceNameUtilTest.java | 6 +- .../generators/model/ShapeClassifierTest.java | 39 ++--- .../model/SubObjectRendererTest.java | 41 ++---- .../model/TransformPipelineTest.java | 1 - .../protocol/JsonProtocolTraitsTest.java | 10 +- .../ProtocolTraitsIncludeSetTest.java | 5 +- .../protocol/ProtocolTraitsSerdeTest.java | 5 +- .../ProtocolTraitsStreamingPayloadTest.java | 8 +- .../model/protocol/XmlProtocolTraitsTest.java | 11 +- .../SmithyEndpointsJmesPathVisitorTest.java | 5 +- .../transforms/DynamoDbTransformsTest.java | 10 +- .../model/transforms/Ec2TransformsTest.java | 1 - .../transforms/LambdaTransformsTest.java | 1 - .../model/transforms/S3TransformsTest.java | 30 ++-- .../transforms/SourceRegionTransformTest.java | 3 +- .../model/transforms/SqsTransformsTest.java | 2 +- .../SupportsPresigningTransformTest.java | 5 +- 77 files changed, 663 insertions(+), 1200 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java index d215e88d875..19259e26f51 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapper.java @@ -34,11 +34,9 @@ private CppTypeMapper() { } /** - * Returns the C++ type/file name for a shape, capitalizing the first character so that - * lowerCamel Smithy shape names (e.g. IAM's {@code statusType}) become UpperCamel C++ - * identifiers ({@code StatusType}). This matches the legacy C2J normalization, which - * upper-camel-cases every shape name at model load - * ({@code C2jModelToGeneratorModelTransformer}). + * Returns the C++ type/file name for a shape, upper-casing the first character so lowerCamel + * Smithy names (e.g. {@code statusType}) become UpperCamel C++ identifiers ({@code StatusType}). + * Matches C2J's model-load normalization. * * @param shape the shape whose C++ type/file name is needed * @return the shape's name with its first character upper-cased @@ -68,12 +66,8 @@ public static String getCppType(Shape shape, Model model) { * @param shape the shape to map * @param model the model (needed to resolve list/map member targets) * @param wideIntegers when {@code true}, {@code integer} maps to {@code int64_t} instead of - * {@code int}. C2J applies this only under the CBOR protocol - * ({@code CORAL_TYPE_TO_CBOR_CPP_TYPE_MAPPING}: {@code integer -> int64_t}), and only - * in the file kinds whose templates set {@code $protocol == "smithy-rpc-v2-cbor"} — - * the CBOR sub-object and result headers. Request headers use the shared - * {@code RequestHeader.vm}, which does not, so they keep {@code int}. {@code long} is - * {@code long long} in every mapping and is unaffected. + * {@code int}. C2J applies this only for CBOR sub-object and result headers; request + * headers keep {@code int}. {@code long} is {@code long long} everywhere, unaffected. */ public static String getCppType(Shape shape, Model model, boolean wideIntegers) { // Check enum BEFORE string — a Smithy 2.0 EnumShape extends StringShape, and a Smithy 1.0 @@ -266,23 +260,20 @@ public static List getIncludesForShape(Shape structureShape, Model model Shape target = model.expectShape(member.getTarget()); if (isRecursiveStructMember(structureShape, target, model)) { // A recursive member is stored as std::shared_ptr. A mutually-referenced T is - // forward-declared (see getForwardDeclarations), so the header needs the allocator - // header for the inline MakeShared setter rather than T's own header. A directly - // self-referential member (T == enclosing) needs neither: the class declares itself - // and MakeShared resolves transitively. Both match C2J. + // forward-declared, so the header needs the allocator header (for the inline + // MakeShared setter) not T's own. A directly self-referential member needs neither. + // Both match C2J. if (!target.getId().equals(selfId)) { includes.add(""); } } else { addMemberInclude(includes, target, selfId, model, projectName); // For list/map, recursively include every nested element/key/value type so leaf - // struct/enum headers reach the surface even through nested containers (e.g. - // apigateway Deployment.apiSummary: Map>). + // struct/enum headers surface even through nested containers. addContainerIncludes(includes, target, selfId, model, projectName); } - // @idempotencyToken members are brace-initialized with - // Aws::Utils::UUID::PseudoRandomUUID(), which requires UUID.h. Matches C2J - // (CppViewHelper.computeMemberIncludeName). + // @idempotencyToken members are brace-initialized with PseudoRandomUUID(), needing + // UUID.h. Matches C2J. if (member.hasTrait(IdempotencyTokenTrait.class)) { includes.add(""); } @@ -303,15 +294,11 @@ private static void addMemberInclude(Set includes, Shape shape, ShapeId } /** - * Recursively adds member-type includes for every nested element/key/value of a list or map - * shape. Recursion only descends through further list/map shapes and stops at - * structures/enums/scalars, so it is bounded by the container-nesting depth (no infinite - * recursion). {@code addMemberInclude} remains a no-op for container/scalar shapes without - * their own header. This lets a member typed, e.g., {@code Map>} - * reach {@code Leaf}'s header, matching C2J's recursive unwrap. + * Recursively adds member-type includes for every nested element/key/value of a list or map, + * descending only through further list/map shapes (bounded by nesting depth). Lets a member + * typed {@code Map>} reach {@code Leaf}'s header. C2J parity. * - *

The {@code @sparse}->{@code } handling fires at each nested - * container level that is sparse, matching C2J's generated headers. + *

{@code @sparse} adds {@code } at each sparse nesting level. */ private static void addContainerIncludes(Set includes, Shape target, ShapeId selfId, Model model, String projectName) { @@ -328,8 +315,8 @@ private static void addContainerIncludes(Set includes, Shape target, Sha addContainerIncludes(includes, key, selfId, model, projectName); addContainerIncludes(includes, value, selfId, model, projectName); } - // A @sparse list/map wraps its element/value in Aws::Crt::Optional, declared in - // . Matches C2J's generated SparseNullsOperationRequest.h. + // A @sparse list/map wraps its element/value in Aws::Crt::Optional (). + // C2J parity. if ((target.isListShape() || target.isMapShape()) && target.hasTrait(SparseTrait.class)) { includes.add(""); } @@ -337,9 +324,8 @@ private static void addContainerIncludes(Set includes, Shape target, Sha /** * Returns the sorted C++ class names of every direct member whose target forms a reference - * cycle with {@code structureShape} (see {@link #isRecursiveStructMember}). These are stored - * as {@code std::shared_ptr} and must be forward-declared (not included) in the header to - * break the otherwise-infinite by-value member. Matches C2J's {@code computeForwardDeclarations}. + * cycle with {@code structureShape}. Stored as {@code std::shared_ptr}, they are + * forward-declared (not included) to break the otherwise-infinite by-value member. C2J parity. * * @param structureShape the enclosing structure/union * @param model the model diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java index 043fb5585ab..9a2d06e0d1f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRenderer.java @@ -69,7 +69,6 @@ public static void renderHeader(CppWriter writer, Shape enumShape, String servic writer.write("namespace $L {", serviceName); writer.write("namespace Model {"); - // Enum class declaration // Use single-line format if it fits within ~140 chars, multi-line otherwise String singleLine = "enum class " + enumName + " { NOT_SET, " + String.join(", ", values) + " };"; @@ -89,7 +88,6 @@ public static void renderHeader(CppWriter writer, Shape enumShape, String servic } writer.write(""); - // Mapper namespace writer.write("namespace $LMapper {", enumName); writer.write("$1L $2L Get$2LForName(const Aws::String& name);", exportMacro, enumName); writer.write(""); @@ -130,14 +128,12 @@ public static void renderSource(CppWriter writer, Shape enumShape, String servic writer.write("namespace $LMapper {", enumName); writer.write(""); - // Hash constants for (int i = 0; i < values.size(); i++) { writer.write(" static const int $1L_HASH = HashingUtils::HashString(\"$2L\");", values.get(i), wireValues.get(i)); } writer.write(""); - // GetForName writer.write(" $1L Get$1LForName(const Aws::String& name) {", enumName); writer.write(" int hashCode = HashingUtils::HashString(name.c_str());"); for (int i = 0; i < values.size(); i++) { @@ -155,7 +151,6 @@ public static void renderSource(CppWriter writer, Shape enumShape, String servic writer.write(" }"); writer.write(""); - // GetNameFor writer.write(" Aws::String GetNameFor$1L($1L enumValue) {", enumName); writer.write(" switch (enumValue) {"); writer.write(" case $1L::NOT_SET:", enumName); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java index d12bfbe576f..918a5fbfcc2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRenderer.java @@ -107,11 +107,10 @@ public void renderPublicAccessors(CppWriter writer) { writer.write("inline const $L& Get$L() const { return $L; }", cppType, methodName, fieldName); } - // The framework-injected ResponseMetadata envelope is always present, so — like C2J — - // it gets no HasBeenSet getter (and its flag is initialized true, below). Every other - // member, including modeled @required ones, tracks presence via HasBeenSet, matching - // C2J's mass-clear of required-ness. `emitHasBeenSet` is the useRequiredField context - // (true for sub-objects/requests, false for results). + // The injected ResponseMetadata envelope is always present, so (like C2J) gets no + // HasBeenSet getter (flag initialized true below). Every other member — including + // @required ones — tracks presence via HasBeenSet. emitHasBeenSet is the + // useRequiredField context (true for sub-objects/requests, false for results). if (emitHasBeenSet && !isInjectedResponseMetadata(member)) { writer.write("inline bool $LHasBeenSet() const { return $LHasBeenSet; }", methodName, fieldName); } @@ -133,9 +132,9 @@ public void renderPublicAccessors(CppWriter writer) { writer.openBlock("void Set$L($L&& value) {", "}", methodName, templateParam, () -> { writer.write("$LHasBeenSet = true;", fieldName); if (recursive) { - // Wrap the value in a shared_ptr, tagged with the enclosing class name for - // the allocator. The template setter is only instantiated at call sites, - // where T is complete, so the header can forward-declare T. Matches C2J. + // Wrap in a shared_ptr tagged with the enclosing class name for the + // allocator. The template setter instantiates only at call sites (where T + // is complete), so the header can forward-declare T. Matches C2J. writer.write("$L = Aws::MakeShared<$L>(\"$L\", std::forward<$L>(value));", fieldName, cppType, className, templateParam); } else { @@ -281,10 +280,8 @@ public static void renderRequestIdAccessors(CppWriter writer, String className) /** * Renders the top-level {@code RequestId} accessor group. When {@code withHasBeenSetGetter} is - * {@code true}, the {@code inline bool RequestIdHasBeenSet() const} getter is emitted after the - * {@code GetRequestId} getter — the MODEL-class variant C2J stamps onto an operation-output - * shape that is also referenced as a member (dual-role sub-object). Result classes pass - * {@code false} (no {@code HasBeenSet} getter), matching {@link #forResult}. + * true, also emits {@code RequestIdHasBeenSet()} — the model-class variant C2J stamps onto a + * dual-role output shape (also referenced as a member). Result classes pass false. */ public static void renderRequestIdAccessors(CppWriter writer, String className, boolean withHasBeenSetGetter) { @@ -309,12 +306,10 @@ public static void renderRequestIdAccessors(CppWriter writer, String className, } /** - * Renders the top-level {@code HostId} (x-amz-id-2) accessor group emitted by S3 Control result - * headers immediately after the {@code RequestId} group: {@code GetHostId} / templated - * {@code SetHostId} / templated {@code WithHostId}. Callers gate emission on - * {@code TopLevelHostIdTrait} and separately emit the {@code m_hostId} field and its - * {@code HasBeenSet} flag in the private section. Matches C2J's {@code addToAllResultsShape} - * HostId member, including the doc string. + * Renders the top-level {@code HostId} (x-amz-id-2) accessor group emitted by S3 Control + * result headers after the {@code RequestId} group. Callers gate on {@code TopLevelHostIdTrait} + * and separately emit the {@code m_hostId} field and its {@code HasBeenSet} flag. Matches C2J's + * {@code addToAllResultsShape} HostId member. */ public static void renderHostIdAccessors(CppWriter writer, String className) { writer.write(""); @@ -336,9 +331,8 @@ public static void renderHostIdAccessors(CppWriter writer, String className) { /** * Writes a single private data member declaration. {@code @idempotencyToken} members are - * brace-initialized with {@code Aws::Utils::UUID::PseudoRandomUUID()} so a caller who omits - * the token still gets idempotent behavior; other members fall back to their type's default - * initializer (or none). Matches C2J's ServiceClientModelHeaderMemberDeclaration.vm. + * brace-initialized with {@code Aws::Utils::UUID::PseudoRandomUUID()} (idempotent even when the + * caller omits the token); others use their type's default initializer or none. C2J parity. */ private static void writeDataMember(CppWriter writer, MemberShape member, String memberName, Model model, boolean wideIntegers, boolean recursive) { @@ -361,11 +355,9 @@ private static void writeDataMember(CppWriter writer, MemberShape member, String } /** - * Writes a single HasBeenSet flag. Matches C2J's ModelClassMembersAndInlines.vm: the flag is - * initialized to {@code true} for an {@code @idempotencyToken} member (auto-populated at - * construction) or a {@code @required} member in a useRequiredField context ({@code emitHasBeenSet} - * — sub-objects/requests, but not results), except when the member is an event stream or a raw - * streaming payload. All other members default to {@code false}. + * Writes a single HasBeenSet flag. Matches C2J: initialized true for an + * {@code @idempotencyToken} member or a {@code @required} member in a useRequiredField context + * ({@code emitHasBeenSet}), unless it is an event stream or raw streaming payload; else false. */ private void writeHasBeenSetFlag(CppWriter writer, MemberShape member, String memberName) { String fieldName = CppNames.fieldName(memberName); @@ -382,13 +374,11 @@ private boolean initialHasBeenSet(MemberShape member) { } /** - * True if this member is the framework-injected {@code ResponseMetadata} envelope - * ({@code GlobalTransforms.injectResponseMetadata}): a member named {@code ResponseMetadata} - * whose target is the injected {@code ResponseMetadata} structure. It is the only member - * rendered as always-present (no {@code HasBeenSet} getter; flag initialized true in a - * HasBeenSet context), matching C2J, which likewise identifies ResponseMetadata by name. - * {@code injectResponseMetadata} fails fast on any modeled ResponseMetadata collision, so this - * name-based check is unambiguous. + * True if this member is the framework-injected {@code ResponseMetadata} envelope: a member + * named {@code ResponseMetadata} whose target is the injected {@code ResponseMetadata} struct. + * It is the only always-present member (no {@code HasBeenSet} getter; flag true in a HasBeenSet + * context). C2J also identifies it by name; {@code injectResponseMetadata} fails fast on any + * modeled collision, so this check is unambiguous. */ private boolean isInjectedResponseMetadata(MemberShape member) { return GlobalTransforms.RESPONSE_METADATA.equals(member.getMemberName()) @@ -416,8 +406,7 @@ private boolean isRawStreamingPayloadMember(MemberShape member) { /** * True if a container element / map key or value is passed to {@code Add*} by value rather - * than by perfect-forwarding reference. Matches C2J: primitive and enum types are by-value - * (they are cheap and trivially copyable), everything else is forwarded. + * than perfect-forwarded. Matches C2J: primitives and enums are by-value, everything else forwarded. */ private static boolean isByValueType(Shape shape) { return CppTypeMapper.isPrimitive(shape) || CppTypeMapper.isEnum(shape); @@ -441,10 +430,9 @@ public static void writeDocComment(CppWriter writer, String doc) { } /** - * Renders the class-level documentation comment for a shape: its {@code @documentation} - * text followed by a "See Also" link to the AWS API reference. Emits an empty doc comment - * ({@code /** *}{@code /}) when the shape carries no documentation. Shared by the - * request, result, sub-object, and event-stream union renderers. + * Renders a shape's class-level doc comment: its {@code @documentation} text plus a "See Also" + * link to the AWS API reference (empty doc comment when undocumented). Shared by the request, + * result, sub-object, and event-stream union renderers. * * @param writer the CppWriter to write to * @param shape the shape whose class doc to render (structure or union) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index 1bf8ecf3831..65905d5e67c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -44,17 +44,14 @@ public String getName() { public void execute(PluginContext context) { Model model = context.getModel(); - // Skip legacy mock projections (no model files to generate) if (context.getProjectionName().endsWith(".mock")) { return; } - // Parse settings ObjectNode settings = context.getSettings(); Map serviceMap = parseMapSetting(settings, "c2jMap"); Map namespaceMap = parseNamespaceMap(settings); - // Build transform pipeline (service-level transforms will be registered here) TransformPipeline pipeline = new TransformPipeline(List.of( GlobalTransforms.asTransform(), SourceRegionTransform.asTransform(), @@ -79,14 +76,12 @@ public void execute(PluginContext context) { ServiceShape processedService = ServiceNameUtil.processS3CrtProjection( service, context.getProjectionName()); - // Apply transforms for this service Model transformedModel = pipeline.apply(model, processedService); String serviceName = ServiceNameUtil.getServiceName(processedService); String smithyServiceName = ServiceNameUtil.getSmithyServiceName(processedService, serviceMap); String exportMacro = ServiceNameUtil.getExportMacro(processedService, serviceMap); - // Resolve namespace override String namespace = namespaceMap.getOrDefault(smithyServiceName, serviceName); ModelGenerator generator = new ModelGenerator( diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java index 396fb3be6bf..843e59563d7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java @@ -8,12 +8,8 @@ import software.amazon.smithy.model.shapes.ServiceShape; /** - * A model-to-model transform applied before code generation. - * - *

Transforms run in sequence. Each receives the model produced by the previous - * transform (or the original model for the first in the chain). Service-level - * transforms (e.g., S3-specific shape mutations) implement this interface and are - * registered in the pipeline. + * A model-to-model transform applied before code generation. Transforms run in sequence, + * each receiving the previous transform's output (or the original model for the first). */ @FunctionalInterface public interface ModelTransform { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolResolver.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolResolver.java index 83b86a5c854..9e33439f476 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolResolver.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolResolver.java @@ -20,21 +20,17 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.protocol.RestXmlProtocolTraits; /** - * Resolves the wire protocol for a Smithy service shape, and maps a resolved - * protocol to the {@link ProtocolTraits} strategy that owns its rendering. - * - *

{@link #traitsFor} is the single protocol-to-behavior switch in the generator; - * downstream generators hold a {@code ProtocolTraits} and never branch on - * {@link Protocol} themselves. + * Resolves the wire protocol for a Smithy service shape and maps it to the {@link ProtocolTraits} + * strategy that owns its rendering. {@link #traitsFor} is the generator's single + * protocol-to-behavior switch; downstream generators hold a {@code ProtocolTraits} and never + * branch on {@link Protocol}. */ public final class ProtocolResolver { /** - * Wire protocol variants supported by the C++ SDK code generator. - * - *

Each variant encapsulates the C++ serde namespace, the type used for - * deserialization (view), the type used for serialization (value), and the - * method name emitted on request shapes. + * Wire protocol variants supported by the C++ SDK code generator. Each carries its C++ serde + * namespace, view (deserialize) type, value (serialize) type, and the serialize method name + * emitted on request shapes. */ public enum Protocol { JSON("Aws::Utils::Json", "Aws::Utils::Json::JsonView", "Aws::Utils::Json::JsonValue", "Jsonize"), @@ -130,10 +126,8 @@ public static Protocol resolve(ServiceShape service, Model model) { /** * Returns the rendering strategy for a resolved protocol. * - *

This is the only place in the generator that switches on - * {@link Protocol}. Every other class receives a {@link ProtocolTraits} and calls - * it, so adding a protocol means adding a case here plus (if its C++ surface is - * genuinely new) one implementation class. + *

The only place in the generator that switches on {@link Protocol}; adding a protocol + * means a case here plus (if its C++ surface is genuinely new) one implementation class. * * @param protocol the protocol returned by {@link #resolve} * @return the strategy that owns this protocol's serde rendering diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RenderContext.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RenderContext.java index 0486fdfd94a..3ad5a649f0d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RenderContext.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RenderContext.java @@ -9,9 +9,8 @@ import software.amazon.smithy.model.shapes.ServiceShape; /** - * Immutable bundle of the per-service inputs every {@link ShapeRenderer} needs. - * Replaces the repeated 6-7 positional constructor arguments so adding a shared - * input is a single-field change rather than an edit to every renderer. + * Immutable bundle of the per-service inputs every {@link ShapeRenderer} needs, so adding a + * shared input is a single-field change rather than an edit to every renderer. * * @param model the transformed Smithy model * @param service the service shape being generated diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java index ecbba4994d8..34812a68b20 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java @@ -73,13 +73,12 @@ public record EventStreamInfo(String operationName, StructureShape requestShape, * @param enums EnumShape or StringShape with @enum trait * @param eventStreamHandlers operation + request/result shape tuples for event stream handlers * @param outgoingEventStreams outgoing event stream shapes (header only) - * @param blobPayloadEvents event structs (members of a {@code @streaming} union) whose sole - * payload is a single {@code @eventPayload} blob member; rendered - * header-only as a blob-carrier event (C2J {@code eventPayloadType == - * "blob"}), never as a JSON sub-object - * @param resultOutputIds shape ids of every operation output; a sub-object whose id is in - * this set is "dual-role" (an output that is also a member) and, for - * JSON-family protocols, receives the C2J {@code requestId} stamp + * @param blobPayloadEvents event structs whose sole payload is a single {@code @eventPayload} + * blob member; rendered header-only as a blob-carrier event (C2J + * {@code eventPayloadType == "blob"}), never a JSON sub-object + * @param resultOutputIds shape ids of every operation output; a sub-object in this set is + * "dual-role" (output also used as a member) and, for JSON-family + * protocols, receives the C2J {@code requestId} stamp */ public record ClassifiedShapes( List requests, @@ -123,9 +122,8 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto // Collect operation inputs/outputs and identify event stream handlers. Deprecated operations // are excluded (matching legacy C2J), so their orphaned request/result structs never emit. for (OperationShape op : GlobalTransforms.nonDeprecatedOperations(model, service)) { - // Use getInputShape() (not getInput()) so no-input operations, whose input - // target is smithy.api#Unit, still produce a RequestInfo. C2J emits a Request - // class for every operation; the generated client method references it. + // Use getInputShape() (not getInput()) so no-input operations (input == smithy.api#Unit) + // still produce a RequestInfo. C2J emits a Request class for every operation. ShapeId inputId = op.getInputShape(); inputShapeIds.add(inputId); model.getShape(inputId).flatMap(Shape::asStructureShape).ifPresent(s -> { @@ -169,9 +167,8 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto } // Structs that are members of a reachable @streaming union — i.e. events. A blob-payload - // event is only recognised among these (analogous to memberTargetIds but restricted to - // event unions), so a plain data struct that merely happens to carry an @eventPayload blob - // is never mis-claimed. + // event is recognised only among these, so a plain data struct carrying an @eventPayload + // blob is never mis-claimed. Set eventStructIds = new HashSet<>(); for (ShapeId id : reachable) { Shape shape = model.expectShape(id); @@ -180,10 +177,9 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto } } - // Incoming event-stream union shape ids: the @streaming union member of every operation - // output collected as an event-stream handler. These unions are realized via the handler - // (EventStreamRenderer.renderHandler{Header,Source}) and never referenced as a data type, - // so their standalone .h is dead public API that we omit. + // Incoming event-stream union shape ids: the @streaming union member of every event-stream + // handler output. Realized via the handler and never referenced as a data type, so their + // standalone .h is dead public API we omit. Set incomingEventStreamUnionIds = new HashSet<>(); for (EventStreamInfo info : eventStreamHandlers) { streamingUnionMember(info.resultShape(), model) @@ -201,8 +197,8 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto // Already collected as an outgoing event stream; do not also render as a data union. } else if (isBlobPayloadEvent(shape, model, eventStructIds)) { // A @streaming-union event whose payload is a single @eventPayload blob member is a - // header-only blob-carrier event (C2J eventPayloadType == "blob"), not a JSON - // sub-object. Routed here before the generic structure branch below. + // header-only blob-carrier event (C2J eventPayloadType == "blob"), routed here + // before the generic structure branch. blobPayloadEvents.add(shape); } else if (shape.hasTrait(ErrorTrait.class)) { if (isModeledException(shape.asStructureShape().get(), protocol)) { @@ -211,11 +207,9 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto } else if (shape.isStructureShape() || shape.isUnionShape()) { // Skip: // - @customRendered shapes (emitted by a dedicated renderer, e.g. DynamoDbRenderer - // for AttributeValue) so the two do not both write — and append into — the same - // model file. - // - empty-member event structs — after EventStreamRenderer's void() callback fix, - // these have no other references and shipping their .h/.cpp adds dead public API - // to the SDK. + // for AttributeValue) so both do not write into the same model file. + // - empty-member event structs — no other references, so their .h/.cpp is dead + // public API. boolean customRendered = shape.hasTrait(CustomRenderedTrait.class); boolean emptyEventStruct = eventStructIds.contains(id) && shape.isStructureShape() @@ -232,14 +226,10 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto } /** - * Determines if an exception shape has members beyond the trivial ones. - * - *

For JSON/CBOR protocols, trivial members are: Message, message. - * For XML protocols, trivial members are: Message, message, Code, code. - * If the exception has any member not in the trivial set, it is "modeled" - * and should generate as a sub-object. Also used by {@code EventStreamRenderer} to decide - * whether an event-stream union's exception member is typed as its concrete shape (modeled) - * or the generic {@code Error} wrapper (non-modeled). + * True if an exception shape has members beyond the trivial ones (Message/message for + * JSON/CBOR; plus Code/code for XML), making it "modeled" and generated as a sub-object. + * Also used by {@code EventStreamRenderer} to type a union's exception member as its concrete + * shape (modeled) or the generic {@code Error} wrapper (non-modeled). */ public static boolean isModeledException(StructureShape shape, Protocol protocol) { Set members = shape.getAllMembers().keySet(); @@ -271,13 +261,9 @@ private static boolean hasRawStreamingPayload(StructureShape shape, Model model) } /** - * True if {@code shape} is a blob-payload event: a structure that is a member of a reachable - * {@code @streaming} union (i.e. an event) and carries a member whose trait set includes - * {@code smithy.api#eventPayload} and whose target is a blob. This mirrors C2J's - * {@code eventPayloadType == "blob"} case (C2jModelToGeneratorModelTransformer): a blob member - * is a raw blob payload only when explicitly {@code @eventPayload}. Non-blob eventPayload - * events (e.g. a CompleteEvent with only string members) are NOT claimed — they remain - * sub-objects. + * True if {@code shape} is a blob-payload event: a member of a reachable {@code @streaming} + * union carrying an {@code @eventPayload} blob member. Mirrors C2J's {@code eventPayloadType == + * "blob"} case; non-blob eventPayload events remain sub-objects. */ private static boolean isBlobPayloadEvent(Shape shape, Model model, Set eventStructIds) { if (!shape.isStructureShape() || !eventStructIds.contains(shape.getId())) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeRenderer.java index 47ae2d878c9..a5cdb1c8718 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeRenderer.java @@ -7,14 +7,9 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; /** - * Strategy interface for rendering C++ code for a specific shape classification. - * - *

Each implementation handles one classification bucket (enum, sub-object, request, - * result, event stream, etc.). The {@link ModelGenerator} dispatches classified shapes - * to the appropriate renderer. - * - *

To add a new classification (e.g., event stream), implement this interface and - * register the renderer in {@link ModelGenerator}. + * Strategy for rendering C++ code for one shape classification bucket (enum, sub-object, request, + * result, event stream, etc.). {@link ModelGenerator} dispatches classified shapes to the matching + * renderer; add a classification by implementing this and registering it there. */ public interface ShapeRenderer { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java index 1a292aa8706..a406574efaf 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/CborProtocolTraits.java @@ -92,9 +92,8 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of("aws/crt/cbor/Cbor.h"); - // All source kinds share one include set. RPC CBOR is an RPC protocol: request sources - // never run the shared @httpQuery / @httpHeader serializers, so they need no URI / - // StringUtils includes — REQUEST_SOURCE carries the same set as every other source kind. + // All source kinds share one include set. RPC CBOR request sources never run the shared + // @httpQuery/@httpHeader serializers, so REQUEST_SOURCE carries the same set as the rest. case REQUEST_SOURCE: case SUBOBJECT_SOURCE: case RESULT_SOURCE: diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java index 02cbe833222..ce9f625a145 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraits.java @@ -89,10 +89,9 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of(); - // Request sources additionally serialize @httpHeader/@httpQuery members via the shared - // serializers, which need URI (AddQueryStringParameter), StringUtils, and - // (std::accumulate for comma-joined list @httpHeader members). These are added only - // here to avoid widening the other source kinds. + // Request sources also serialize @httpHeader/@httpQuery members, needing URI, + // StringUtils, and (std::accumulate for comma-joined list headers). Added + // only here to avoid widening other source kinds. case REQUEST_SOURCE: return List.of( "aws/core/utils/json/JsonSerializer.h", @@ -103,8 +102,7 @@ public List serdeIncludes(FileKind kind) { "aws/core/http/URI.h", "numeric", "utility"); - // All source kinds share one union (supersets allowed: a .cpp may carry an - // include it doesn't strictly use). Usings are unchanged; only #includes widen. + // All source kinds share one union (supersets allowed). Only #includes widen; usings unchanged. case SUBOBJECT_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: @@ -213,10 +211,7 @@ public void writeRequestMethodImpls(CppWriter writer, String className, writer.write(""); writeAddQueryStringParametersImpl(writer, className, shape, model); } - // A presignable operation (e.g. Polly's SynthesizeSpeech) declares the protocol-agnostic - // DumpBodyToUrl override (emitted by RequestRenderer, gated on the same operation trait); the - // real body defers with serde, as SerializePayload does, so this is a stub. - // UnreferencedParam.h is in serdeIncludes(REQUEST_SOURCE). + // DumpBodyToUrl stub for presigning-capable operations; body serialization pending schema serde. if (operation.hasTrait(SupportsPresigningTrait.class)) { writer.write(""); writer.write("void $L::DumpBodyToUrl(Aws::Http::URI& uri) const { AWS_UNREFERENCED_PARAM(uri); }", diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java index f144e81d2e1..c3c375ce9e5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraits.java @@ -21,10 +21,9 @@ /** * Owns every protocol-specific rendering decision for generated model code. * - *

Renderers receive a {@code ProtocolTraits} and call these methods; they never - * branch on {@link Protocol} themselves. One implementation exists per serde - * family (JSON-like, REST-XML, query-XML), not per protocol, so the - * conditional arms that used to be repeated across renderers are now classes. + *

Renderers receive a {@code ProtocolTraits} and call these methods; they never branch on + * {@link Protocol}. One implementation exists per serde family (JSON-like, REST-XML, + * query-XML), not per protocol. * *

Obtain an instance from * {@code ProtocolResolver.traitsFor(ProtocolResolver.resolve(service, model))}. @@ -153,36 +152,28 @@ default boolean hasTargetHeader() { /** * Whether this protocol honors HTTP binding traits ({@code @httpHeader} / * {@code @httpPrefixHeaders} / {@code @httpQuery} / {@code @httpQueryParams}) by serializing - * those members onto the wire (request headers / query string). + * those members onto the wire. * - *

REST protocols (rest-json, rest-xml, query/ec2) return {@code true}. RPC protocols - * (awsJson1_0/1_1, rpcv2Cbor) route these members into the request body instead, so - * they return {@code false}: their {@code GetRequestSpecificHeaders} still emits the fixed - * protocol headers ({@code X-Amz-Target} for awsJson; {@code Content-Type}/{@code smithy-protocol}/ - * {@code Accept} for CBOR), but no member header/query serialization is emitted, and no - * {@code AddQueryStringParameters} method is generated. Matches the legacy C2J per-protocol - * behavior (byte-parity reference). + *

REST protocols return {@code true}. RPC protocols (awsJson, rpcv2Cbor) route these members + * into the body and return {@code false}: they still emit fixed protocol headers but no member + * header/query serialization and no {@code AddQueryStringParameters}. C2J parity. */ default boolean serializesHttpBindingMembers() { return true; } /** - * Whether {@code integer} members widen to {@code int64_t} (rather than {@code int}) in this - * protocol's sub-object and result headers. C2J does this only for CBOR - * ({@code CORAL_TYPE_TO_CBOR_CPP_TYPE_MAPPING}: {@code integer -> int64_t}, applied where the - * template sets {@code $protocol == "smithy-rpc-v2-cbor"}). Request headers use the shared - * {@code RequestHeader.vm}, which does not widen, so this never affects request members. + * Whether {@code integer} members widen to {@code int64_t} in this protocol's sub-object and + * result headers. C2J does this only for CBOR; request headers never widen. */ default boolean widensIntegers() { return false; } /** - * Whether result classes for this protocol expose a top-level {@code GetRequestId()} / - * {@code m_requestId} accessor. C2J emits it for every protocol except Query/EC2, - * whose results instead carry the request id inside the injected {@code ResponseMetadata} - * member. Query/EC2 override this to {@code false}. + * Whether result classes expose a top-level {@code GetRequestId()} / {@code m_requestId} + * accessor. C2J emits it for every protocol except Query/EC2 (which carry the request id inside + * the injected {@code ResponseMetadata}); Query/EC2 override to {@code false}. */ default boolean resultHasTopLevelRequestId() { return true; @@ -207,17 +198,14 @@ default void writeGetRequestSpecificHeadersImpl(CppWriter writer, String classNa service.getId().getName(), operation.getId().getName()); } // Per-service constant request headers (C2J metadata.additionalHeaders, e.g. Glacier's - // x-amz-glacier-version). Streaming requests derive from AmazonStreamingWebServiceRequest - // and bypass Request::GetHeaders, so these are emitted here — matching - // StreamRequestSource.vm, which inserts them after X-Amz-Target and before the - // member-driven headers. The trait is stamped only on request shapes that need it. + // x-amz-glacier-version). Streaming requests bypass Request::GetHeaders, so + // these are emitted here (after X-Amz-Target, before member-driven headers). Trait-gated. shape.getTrait(AdditionalRequestHeadersTrait.class).ifPresent(trait -> trait.getHeaders().forEach((name, value) -> writer.write("headers.insert(Aws::Http::HeaderValuePair(\"$L\", \"$L\"));", name, value))); - // C2J declares the stringstream once, immediately after `headers`, whenever the request - // has ≥1 header member (even all-enum/timestamp members, which never use it). RPC - // protocols route HTTP-binding members to the body, so neither the stringstream nor the - // member serialization is emitted for them. + // C2J declares the stringstream once after `headers` whenever the request has >=1 + // header member. RPC protocols route HTTP-binding members to the body, so they emit + // neither the stringstream nor member serialization. if (serializesHttpBindingMembers() && RequestBindings.hasHeaderMembers(shape, model)) { writer.write("Aws::StringStream ss;"); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java index 19c923cb95b..7d9cd84642e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/QueryXmlProtocolTraits.java @@ -96,10 +96,9 @@ public List serdeIncludes(FileKind kind) { case RESULT_HEADER: // Query/EC2 result headers forward-declare XmlDocument; no serde include. return List.of(); - // Request sources additionally serialize @httpHeader/@httpQuery members via the shared - // serializers, which need URI (AddQueryStringParameter), StringUtils, the stringstream, - // and (std::accumulate for comma-joined list @httpHeader members). URI.h is - // added only here to avoid widening the other source kinds. + // Request sources also serialize @httpHeader/@httpQuery members, needing URI, + // StringUtils, the stringstream, and (std::accumulate for comma-joined list + // headers). URI.h added only here to avoid widening other source kinds. case REQUEST_SOURCE: return List.of( "aws/core/utils/xml/XmlSerializer.h", @@ -217,9 +216,8 @@ public void writeRequestMethodDecls(CppWriter writer, String exportMacro, writer.write(""); writeAddQueryStringParametersDecl(writer, exportMacro); } - // The protected DumpBodyToUrl override declaration is emitted protocol-agnostically by - // RequestRenderer (gated on the operation's SupportsPresigningTrait, stamped on every - // query/ec2 operation including Unit-input ops); only the impl below is protocol-specific. + // The DumpBodyToUrl override decl is emitted protocol-agnostically by RequestRenderer + // (gated on SupportsPresigningTrait); only the impl below is protocol-specific. } @Override @@ -237,8 +235,8 @@ public void writeRequestMethodImpls(CppWriter writer, String className, writer.write(""); writeAddQueryStringParametersImpl(writer, className, shape, model); } - // Gate the DumpBodyToUrl impl on the same operation trait as the RequestRenderer decl so the - // two stay symmetric: a Unit-input op with the trait gets both, an op without it gets neither. + // Gate the DumpBodyToUrl impl on the same trait as the RequestRenderer decl so the two stay + // symmetric. if (operation.hasTrait(SupportsPresigningTrait.class)) { writer.write(""); writer.write("void $L::DumpBodyToUrl(Aws::Http::URI& uri) const { uri.SetQueryString(SerializePayload()); }", diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java index db5838d18e9..9522dce53d1 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/RestXmlProtocolTraits.java @@ -75,10 +75,9 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of(); - // Request sources additionally serialize @httpHeader/@httpQuery members, which need - // StringUtils (to_string), URI (URLEncodePath for x-amz-copy-source), and - // (std::accumulate for comma-joined list headers). C2J pulls these per-shape; the - // data-driven set carries them for every request source (superset). + // Request sources also serialize @httpHeader/@httpQuery members, needing StringUtils, + // URI (URLEncodePath), and (std::accumulate for comma-joined list headers). + // Carried for every request source (superset). case REQUEST_SOURCE: return List.of( "aws/core/utils/xml/XmlSerializer.h", @@ -199,11 +198,8 @@ private void writeHasEmbeddedErrorDecl(CppWriter writer, String exportMacro) { + "const Http::HeaderValueCollection &header) const override;", exportMacro); } - // Constant XML error-sniff body, identical across C2J's S3 request-source templates - // (XmlRequestSource / StreamRequestSource / PutBucketNotificationConfigurationRequest): parse the - // response body as XML and report an embedded error when the root element is . It is not - // shape-dependent, so there is nothing to defer. XmlSerializer.h + UnreferencedParam.h and the - // Aws::Utils::Xml / Aws::Utils usings are already in the REQUEST_SOURCE serde includes/usings. + // Constant XML error-sniff body, identical across C2J's S3 request-source templates: parse the + // body as XML and report an embedded error when the root element is . Not shape-dependent. private void writeHasEmbeddedErrorImpl(CppWriter writer, String className) { writer.openBlock("bool $L::HasEmbeddedError(Aws::IOStream& body, " + "const Aws::Http::HeaderValueCollection& header) const {", "}", className, () -> { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java index dd89c780818..d7aecfaaf27 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/DynamoDbRenderer.java @@ -14,10 +14,9 @@ /** * Emits DynamoDB's bespoke document-type {@code AttributeValue} / {@code AttributeValueValue} - * classes, matching the legacy C2J {@code DynamoDBJsonCppClientGenerator}. The four files are - * static hand-written C++ (no model-driven content); their bodies live as classpath resources and - * are written verbatim. No-op for every non-DynamoDB service. The default union rendering of the - * {@code AttributeValue} shape is suppressed in {@code ModelGenerator}. + * classes (C2J {@code DynamoDBJsonCppClientGenerator}). The four files are static hand-written C++ + * written verbatim from classpath resources; no-op for non-DynamoDB services. Default union + * rendering of {@code AttributeValue} is suppressed in {@code ModelGenerator}. */ public final class DynamoDbRenderer implements ShapeRenderer { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java index 820f6644f81..8f5fa136b1b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventPayloadRenderer.java @@ -21,14 +21,13 @@ /** * Renders header-only blob-carrier events: an event struct (member of a {@code @streaming} union) - * whose sole payload is a single {@code @eventPayload} blob member (C2J {@code eventPayloadType == - * "blob"}). C2J renders these via {@code EventHeader.vm} as a plain value type carrying an - * {@code Aws::Vector} payload — a bytes constructor, non-template const-ref / rvalue - * accessors, and a {@code GetWithOwnership()} move-out — with NO {@code Jsonize} / - * {@code JsonView} serde and NO {@code .cpp}. + * whose sole payload is one {@code @eventPayload} blob member (C2J {@code eventPayloadType == "blob"}). + * C2J renders these via {@code EventHeader.vm} as a plain value type over an + * {@code Aws::Vector} (bytes ctor, const-ref/rvalue accessors, a + * {@code GetWithOwnership()} move-out), with no serde and no {@code .cpp}. * - *

These shapes are routed here by {@link ShapeClassifier} instead of {@code subObjects}, so the - * generic {@code SubObjectRenderer} JSON path never sees them. + *

{@link ShapeClassifier} routes these here instead of {@code subObjects}, so the generic + * {@code SubObjectRenderer} JSON path never sees them. */ public final class EventPayloadRenderer implements ShapeRenderer { @@ -103,9 +102,8 @@ private void renderHeader(CppWriterDelegator writerDelegator, StructureShape sha } /** - * Emits the payload member's doc comment. Matches C2J's {@code EventHeader.vm}, which always - * renders a {@code /** ... *}{@code /} block from the member's {@code @documentation} - * (whitespace-collapsed). + * Emits the payload member's doc comment (whitespace-collapsed {@code @documentation}), matching + * C2J {@code EventHeader.vm}. */ private void writeMemberDoc(CppWriter writer, MemberShape payload) { String doc = payload.getTrait(DocumentationTrait.class) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java index 0ffab02d1c0..805134b4a40 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java @@ -24,15 +24,11 @@ import java.util.Optional; /** - * Renders C++ event stream artifacts for response-side (simplex) streaming operations: - * the handler and initial response. Driven by the classifier's {@link EventStreamInfo} - * list. Event structure shapes themselves are generated elsewhere (as reachable - * sub-objects) and only referenced here. The {@code @streaming} union data type is not - * emitted: it is realized entirely through the handler and referenced by nothing, so the - * classifier drops it from sub-objects (dead public API). - * - *

No protocol-specific serialization is emitted; payload (de)serialization points - * are protocol-agnostic TODO stubs via {@link ProtocolTraits}. + * Renders C++ event stream artifacts for response-side (simplex) streaming operations: the handler + * and initial response, driven by the classifier's {@link EventStreamInfo} list. Event structures + * are generated elsewhere (as reachable sub-objects) and only referenced here; the {@code @streaming} + * union data type is not emitted (realized through the handler, referenced by nothing). Payload + * (de)serialization points are protocol-agnostic TODO stubs via {@link ProtocolTraits}. */ public final class EventStreamRenderer implements ShapeRenderer { @@ -123,7 +119,6 @@ private void renderHandlerHeader(CppWriterDelegator writerDelegator, String opNa writer.write(""); ModelFile.modelNamespace(writer, ctx.namespace(), () -> { - // EventType enum StringBuilder enumBody = new StringBuilder("enum class ") .append(opName).append("EventType { INITIAL_RESPONSE, "); for (MemberShape event : events) { @@ -134,7 +129,6 @@ private void renderHandlerHeader(CppWriterDelegator writerDelegator, String opNa writer.write(""); writer.openBlock("class $1L : public Aws::Utils::Event::EventStreamHandler {", "};", className, () -> { - // Callback typedefs writer.write("typedef std::function $1LInitialResponseCallback;", opName); writer.write("typedef std::function $1LInitialResponseCallbackEx;", opName); for (MemberShape event : events) { @@ -236,7 +230,6 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa writer.write("static const char $1L[] = \"$2LHandler\";", tag, opName); writer.write(""); - // Constructor writer.openBlock("$1L::$1L() : EventStreamHandler() {", "}", className, () -> { writer.openBlock("m_onInitialResponse = [&](const $1LInitialResponse&, const Utils::Event::InitialResponseType eventType) {", "};", opName, () -> { writer.write("AWS_LOGSTREAM_TRACE($1L, \"$2L initial response received from \" << (eventType == Utils::Event::InitialResponseType::ON_EVENT ? \"event\" : \"http headers\"));", tag, opName); @@ -260,7 +253,6 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa }); writer.write(""); - // OnEvent writer.openBlock("void $1L::OnEvent() {", "}", className, () -> { writer.openBlock("if (!*this) {", "}", () -> { writer.write("AWSError error = EventStreamErrorsMapper::GetAwsErrorForEventStreamError(GetInternalError());"); @@ -290,7 +282,6 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa }); writer.write(""); - // HandleEventInMessage writer.openBlock("void $1L::HandleEventInMessage() {", "}", className, () -> { writer.write("const auto& headers = GetEventHeaders();"); writer.write("auto eventTypeHeaderIter = headers.find(EVENT_TYPE_HEADER);"); @@ -323,7 +314,6 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa }); writer.write(""); - // HandleErrorInMessage writer.openBlock("void $1L::HandleErrorInMessage() {", "}", className, () -> { writer.write("const auto& headers = GetEventHeaders();"); writer.write("Aws::String errorCode;"); @@ -372,7 +362,6 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa }); writer.write(""); - // EventMapper writer.writeNamespaceOpen(opName + "EventMapper"); writer.write("static const int INITIAL_RESPONSE_HASH = Aws::Utils::HashingUtils::HashString(\"initial-response\");"); for (MemberShape event : events) { @@ -413,9 +402,8 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa // ---- Initial response --------------------------------------------------- /** - * Builds a synthetic {@code InitialResponse} structure from the result's non-event-stream - * members, mirroring C2J's {@code addEventStreamInitialResponse} (CppClientGenerator). The - * {@code @streaming} union member (the event stream) is excluded. + * Builds a synthetic {@code InitialResponse} from the result's non-event-stream members + * (the {@code @streaming} union member excluded), mirroring C2J {@code addEventStreamInitialResponse}. */ private StructureShape initialResponseShape(String opName, StructureShape resultShape) { StructureShape.Builder builder = StructureShape.builder() diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/IncludeSets.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/IncludeSets.java index 4b9e8f41032..94b46af1789 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/IncludeSets.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/IncludeSets.java @@ -15,9 +15,8 @@ * Universal (non-serde, non-member) {@code #include} paths for each generated file kind. * Paths are returned without angle brackets; callers add {@code #include <...>}. * - *

These are the boilerplate includes that were previously hand-written as string literals - * in each renderer. Serde includes come from {@code ProtocolTraits.serdeIncludes}; member - * includes come from {@code CppTypeMapper.getIncludesForShape}. + *

Serde includes come from {@code ProtocolTraits.serdeIncludes}; member includes come + * from {@code CppTypeMapper.getIncludesForShape}. */ final class IncludeSets { @@ -65,9 +64,7 @@ static List streamingResultSourceBase(String smithyServiceName, String c static List requestSourceBase(String smithyServiceName, String className) { List inc = new ArrayList<>(); inc.add("aws/" + smithyServiceName + "/model/" + className + ".h"); - // NOTE: is NOT added here. Query/EC2 request sources must not include it, - // and the non-Query protocols' serdeIncludes(REQUEST_SOURCE) already carry . - // Adding it here would force it onto Query. + // NOT added: Query/EC2 must omit it, and non-Query serdeIncludes(REQUEST_SOURCE) already carry it. return inc; } @@ -79,11 +76,9 @@ static List subObjectSourceBase(String smithyServiceName, String classNa } /** - * Assembles and emits a source file's {@code #include} block: the per-site {@code base} - * (self-header, {@code AmazonWebServiceResult.h}, etc.) plus the protocol's source-include - * union for {@code kind}. The two lists are concatenated then emitted via {@link #emit}, - * which dedups, sorts (CaseSensitive) and brackets. Usings are emitted separately by the - * caller via {@link #emitUsings} — this method never touches usings. + * Emits a source file's {@code #include} block: {@code base} plus the protocol's source + * includes for {@code kind}, via {@link #emit} (dedup/sort/bracket). Usings are the caller's + * job ({@link #emitUsings}); this method never touches them. */ static void emitSourceIncludes(CppWriter writer, List base, ProtocolTraits traits, FileKind kind) { @@ -102,9 +97,9 @@ static void emit(com.amazonaws.util.awsclientsmithygenerator.generators.CppWrite } /** - * Emits {@code #include } for each path, normalizing brackets so a caller may - * pass either {@code } or {@code aws/x/X.h}. Deduped and sorted CaseSensitive, - * matching {@link #emit}. This is the single place bracket policy for header includes lives. + * Emits {@code #include } per path, normalizing brackets so callers may pass + * {@code } or {@code aws/x/X.h}. Deduped/sorted like {@link #emit}; the single + * place header bracket policy lives. */ static void emitAngleIncludes(com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter writer, java.util.Collection includePaths) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ModelFile.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ModelFile.java index 8e21fe78903..a8f0d67989d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ModelFile.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ModelFile.java @@ -7,10 +7,9 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; /** - * Emits the {@code Aws::::Model} nesting shared by every generated model - * file. Callers supply the class body; the triple open/close is owned here so it can - * never drift out of sync. Files that must interleave forward declarations between - * {@code Aws} and {@code } use {@link CppWriter#withNamespace} directly. + * Emits the {@code Aws::::Model} nesting shared by every generated model file. Callers + * supply the class body; the triple open/close is owned here so it can't drift. Files interleaving + * forward declarations between {@code Aws} and {@code } use {@link CppWriter#withNamespace}. */ final class ModelFile { @@ -23,10 +22,9 @@ static void modelNamespace(CppWriter writer, String namespace, Runnable body) { } /** - * Like {@link #modelNamespace(CppWriter, String, Runnable)} but emits {@code awsProlog} - * directly inside {@code Aws} — before opening {@code namespace} — for headers that must - * place forward declarations (e.g. {@code AmazonWebServiceResult}, serde value types) at - * {@code Aws} scope. The body still renders inside {@code Aws::::Model}. + * Like {@link #modelNamespace(CppWriter, String, Runnable)} but emits {@code awsProlog} at + * {@code Aws} scope (before {@code namespace}) for headers needing forward declarations there + * (e.g. {@code AmazonWebServiceResult}). The body still renders in {@code Aws::::Model}. */ static void modelNamespace(CppWriter writer, String namespace, Runnable awsProlog, Runnable body) { writer.withNamespace("Aws", () -> { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/OutgoingEventStreamRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/OutgoingEventStreamRenderer.java index df03be05ff9..d7e7679c2b2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/OutgoingEventStreamRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/OutgoingEventStreamRenderer.java @@ -23,14 +23,11 @@ import java.util.Map; /** - * Renders outgoing (request-side / bidirectional) event streams: a {@code @streaming} union - * bound to an operation input. C2J emits these header-only as an - * {@code Aws::Utils::Event::EventEncoderStream} subclass with one {@code Write(...)} - * method per union member (EventStreamHeader.vm), rather than as a tagged-union data type. - * - *

Only the structure/list payload serialization is protocol-specific; that arm is delegated - * to {@link ProtocolTraits#writeStructureEventPayload}. Blob and string payloads write the same - * way for every protocol. + * Renders outgoing (request-side / bidirectional) event streams: a {@code @streaming} union bound + * to an operation input. C2J emits these header-only (EventStreamHeader.vm) as an + * {@code EventEncoderStream} subclass with one {@code Write(...)} per union member, not a + * tagged-union data type. Only structure/list payloads are protocol-specific (delegated to + * {@link ProtocolTraits#writeStructureEventPayload}); blob/string payloads write uniformly. */ public final class OutgoingEventStreamRenderer implements ShapeRenderer { @@ -132,10 +129,9 @@ private void writeEventHeaders(CppWriter writer, String wireKey) { } /** - * Determines how an event shape's payload is encoded, mirroring C2J's eventPayloadType logic - * (C2jModelToGeneratorModelTransformer). A single blob/string non-header member serializes as - * that member; a member explicitly marked {@code @eventPayload} likewise; otherwise the event - * structure itself is the payload (structure encoding). + * How an event's payload is encoded, mirroring C2J eventPayloadType: a single blob/string + * non-header member (or an explicit {@code @eventPayload} member) serializes as that member; + * otherwise the event structure itself is the payload. */ private PayloadKind payloadKind(StructureShape event) { List> nonHeader = event.getAllMembers().entrySet().stream() @@ -150,8 +146,7 @@ private PayloadKind payloadKind(StructureShape event) { if (target.isStringShape()) { return PayloadKind.STRING; } - // A blob member is written as a raw blob only when explicitly @eventPayload; an - // implicit single blob member makes the parent structure the payload (matches C2J). + // Implicit single blob member => parent is the payload (C2J); raw blob only when explicitly @eventPayload if (target.isBlobShape() && member.hasTrait(EventPayloadTrait.class)) { return PayloadKind.BLOB; } @@ -160,9 +155,8 @@ private PayloadKind payloadKind(StructureShape event) { } /** - * The single non-header payload member name for the blob/string arms. These arms are only - * reached when {@link #payloadKind} found exactly one such member, so absence is a codegen - * bug rather than a modeled state — fail fast. + * The single non-header payload member name for the blob/string arms; reached only when + * {@link #payloadKind} found exactly one, so absence is a codegen bug — fail fast. */ private String requirePayloadMember(StructureShape event, PayloadKind kind) { return event.getAllMembers().entrySet().stream() diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java index 82cd84eed55..12bcdd43ef1 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestHeaderSerializer.java @@ -19,36 +19,29 @@ import java.util.Locale; /** - * Emits the {@code @httpHeader} member-serialization loop body for a request (operation-input) - * structure's {@code GetRequestSpecificHeaders()}, byte-matching the legacy C2J - * {@code ModelClassHeaderMembersSource.vm}. + * Emits the {@code @httpHeader} member-serialization loop body for a request's + * {@code GetRequestSpecificHeaders()}, byte-matching C2J {@code ModelClassHeaderMembersSource.vm}. * - *

Protocol-agnostic: the member serialization is byte-identical across REST-XML, JSON, - * REST-JSON, Query-XML, EC2, and CBOR, so this renderer never branches on protocol. The - * caller ({@code ProtocolTraits.writeGetRequestSpecificHeadersImpl}) owns the surrounding - * {@code Aws::Http::HeaderValueCollection headers;} / {@code Aws::StringStream ss;} declarations, - * the protocol prologue (e.g. {@code X-Amz-Target}), and {@code return headers;}. + *

Protocol-agnostic (byte-identical across REST-XML, JSON, REST-JSON, Query-XML, EC2, CBOR). + * The caller owns the surrounding {@code headers}/{@code ss} declarations, protocol prologue, and + * {@code return headers;}. Every member is {@code HasBeenSet}-gated; enums also guard {@code ::NOT_SET}. * - *

Every member is {@code HasBeenSet}-gated (C2J clears {@code required} on all members), and - * enum members additionally guard against {@code ::NOT_SET}. - * - *

Scope: {@code @httpHeader} members (string / {@code x-amz-copy-source} / enum / boolean / - * blob / timestamp scalars, plus lists joined via {@code std::accumulate}) and - * {@code @httpPrefixHeaders} maps (looped, with sparse-value {@code has_value()} unwrapping). + *

Scope: {@code @httpHeader} scalars (string / {@code x-amz-copy-source} / enum / boolean / blob / + * timestamp) and lists (joined via {@code std::accumulate}), plus {@code @httpPrefixHeaders} maps + * (looped, sparse values unwrapped via {@code has_value()}). */ public final class RequestHeaderSerializer { private RequestHeaderSerializer() {} /** - * Emits the header-member serialization for every header-bound member of {@code shape}, in - * model order: {@code @httpHeader} scalars/lists, and {@code @httpPrefixHeaders} maps. Members - * carrying neither trait are skipped. + * Emits header serialization for every header-bound member of {@code shape} in model order: + * {@code @httpHeader} scalars/lists and {@code @httpPrefixHeaders} maps; others skipped. */ public static void render(CppWriter writer, StructureShape shape, Model model) { for (MemberShape member : shape.getAllMembers().values()) { - // C2J lowercases HTTP header location names (and @httpPrefixHeaders prefixes); query - // names stay case-sensitive. Locale.ROOT avoids locale-dependent casing surprises. + // C2J lowercases header location names (and @httpPrefixHeaders prefixes); Locale.ROOT + // avoids locale-dependent casing. member.getTrait(HttpHeaderTrait.class).ifPresent(trait -> renderHeaderMember(writer, member, trait.getValue().toLowerCase(Locale.ROOT), model)); member.getTrait(HttpPrefixHeadersTrait.class).ifPresent(trait -> @@ -80,9 +73,8 @@ private static void renderHeaderMember(CppWriter writer, MemberShape member, Str renderScalarBody(writer, field, location, target, model)); } - // @httpPrefixHeaders map: each entry becomes a header whose name is the trait prefix - // concatenated with the entry key. A @sparse map's value is Aws::Crt::Optional, so the emplace - // is guarded on has_value() and unwrapped via value(). + // @httpPrefixHeaders map: each entry becomes a header named prefix+key. A @sparse value is + // Aws::Crt::Optional, so the emplace is guarded on has_value() and unwrapped via value(). private static void renderPrefixHeadersMap(CppWriter writer, MemberShape member, String prefix, Model model) { Shape target = model.expectShape(member.getTarget()); @@ -113,11 +105,10 @@ private static void renderListBody(CppWriter writer, String field, String locati writer.write(" }));"); } - // Shared per-type header value expression, keyed on the target shape: enum → Mapper lookup, - // timestamp → the header timestamp mapping (epoch-seconds→Seconds(), date-time→ISO_8601, - // else→RFC822), primitive → to_string, any other (string) value used directly. The value - // expression (the field for a scalar member, the loop var for a list element) is the parameter, - // so the scalar and list paths share one copy of the enum-Mapper and timestamp-format mappings. + // Shared per-type header value expression, keyed on target shape: enum → Mapper lookup, + // timestamp → header mapping (epoch-seconds→Seconds(), date-time→ISO_8601, else→RFC822), + // primitive → to_string, else (string) used directly. The value expr (scalar field or list + // loop var) is the parameter, so scalar and list paths share this mapping. private static String headerValueExpression(Shape shape, String valueExpr, Model model) { if (CppTypeMapper.isEnum(shape)) { String enumType = CppTypeMapper.getCppType(shape, model, false); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java index ce686fa91be..9ff723772d9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestQuerySerializer.java @@ -17,39 +17,32 @@ import software.amazon.smithy.model.traits.TimestampFormatTrait; /** - * Emits the {@code @httpQuery} member-serialization loop body for a request (operation-input) - * structure's {@code AddQueryStringParameters(Aws::Http::URI&)}, byte-matching the legacy C2J + * Emits the {@code @httpQuery} member-serialization loop body for a request's + * {@code AddQueryStringParameters(Aws::Http::URI&)}, byte-matching C2J * {@code AddQueryStringParametersToRequest.vm}. * - *

Protocol-agnostic: the query member serialization is byte-identical across REST-XML, - * Query-XML, EC2, JSON, and CBOR, so this renderer never branches on protocol. The caller - * ({@code ProtocolTraits.writeAddQueryStringParametersImpl}) owns the surrounding - * {@code Aws::StringStream ss;} declaration and the method scaffold; {@code uri} is the - * {@code Aws::Http::URI&} method parameter. + *

Protocol-agnostic (byte-identical across REST-XML, Query-XML, EC2, JSON, CBOR). The caller owns + * the surrounding {@code ss} declaration and method scaffold; {@code uri} is the method parameter. + * Every member is {@code HasBeenSet}-gated; unlike headers, query enums are NOT guarded against + * {@code ::NOT_SET} (C2J's query template gates on {@code HasBeenSet} only). * - *

Every member is {@code HasBeenSet}-gated (C2J clears {@code required} on all members). Unlike - * headers, query enum members are NOT additionally guarded against {@code ::NOT_SET} — C2J's query - * template ({@code AddQueryStringParameter.vm}) gates on {@code HasBeenSet} only. + *

Scope: scalar / string / enum / timestamp {@code @httpQuery} members, {@code @httpQuery} lists + * (one query parameter per element under the fixed location), and {@code @httpQueryParams} maps + * (each entry keyed by its own key — scalar value, enum key via {@code Mapper}, or list value via an + * inner loop). Every query case routes through the shared {@code ss}. Query timestamps default to + * {@code date-time} (ISO_8601), unlike the header default (RFC822). * - *

Scope: scalar / string / enum / timestamp {@code @httpQuery} members, {@code @httpQuery} - * lists (looped, one query parameter per element under the fixed location), and - * {@code @httpQueryParams} maps (looped, each entry keyed by the map's own key — scalar value, - * enum key via {@code Mapper}, or list value via an inner loop). Unlike headers, every query - * case routes its value through the shared {@code ss} stringstream. The query timestamp default - * is {@code date-time} (ISO_8601), differing from the header default (RFC822). - * - *

The S3 {@code customizedAccessLogTag} member (stamped with {@link CustomizedAccessLogTagTrait} - * by {@code S3Transforms}) is a special case: C2J models it with a {@code customizedQuery} flag, so - * it is skipped in the normal {@code @httpQueryParams} loop and instead emits an {@code x-}-prefix - * filter block once after the loop, byte-matching {@code AddQueryStringParametersToRequest.vm}. + *

The S3 {@code customizedAccessLogTag} member ({@link CustomizedAccessLogTagTrait}, C2J's + * {@code customizedQuery} flag) is special: skipped in the normal {@code @httpQueryParams} loop, it + * instead emits an {@code x-}-prefix filter block once after the loop. */ public final class RequestQuerySerializer { private RequestQuerySerializer() {} /** - * Emits the query-member serialization for every {@code @httpQuery} scalar/string/enum/timestamp - * member of {@code shape}, in model order. Members carrying no {@code @httpQuery} trait are skipped. + * Emits query serialization for every {@code @httpQuery} member of {@code shape} in model order; + * members without the trait are skipped. */ public static void render(CppWriter writer, StructureShape shape, Model model) { for (MemberShape member : shape.getAllMembers().values()) { @@ -57,8 +50,8 @@ public static void render(CppWriter writer, StructureShape shape, Model model) { renderQueryMember(writer, member, trait.getValue(), model)); member.getTrait(HttpQueryParamsTrait.class).ifPresent(trait -> { // The S3 customizedAccessLogTag member carries @httpQueryParams (to keep the request - // emitting AddQueryStringParameters) but is not serialized as a normal map — it emits - // the x- filter block after this loop instead. + // emitting AddQueryStringParameters) but emits the x- filter block after this loop + // instead of serializing as a normal map. if (!member.hasTrait(CustomizedAccessLogTagTrait.class)) { renderQueryParamsMap(writer, member, model); } @@ -125,9 +118,9 @@ private static void emitStreamed(CppWriter writer, String location, String strea writer.write("ss.str(\"\");"); } - // @httpQueryParams map: each entry becomes a query parameter keyed by the map's own key - // (there is no fixed location). A scalar value streams directly; a list value fans out to one - // query parameter per element via an inner loop; an enum key is mapped through its Mapper. + // @httpQueryParams map: each entry becomes a query parameter keyed by its own key (no fixed + // location). Scalar value streams directly; list value fans out one parameter per element; enum + // key is mapped through its Mapper. private static void renderQueryParamsMap(CppWriter writer, MemberShape member, Model model) { Shape target = model.expectShape(member.getTarget()); String field = CppNames.fieldName(member.getMemberName()); @@ -152,9 +145,8 @@ private static void renderQueryParamsMap(CppWriter writer, MemberShape member, M })); } - // Query parameter key for an @httpQueryParams entry: an enum key routes through its Mapper, - // any other (string) key uses the raw entry key. Both terminate in .c_str() since - // AddQueryStringParameter takes a const char* key. + // Query parameter key for an @httpQueryParams entry: enum key via its Mapper, else the raw entry + // key. Both end in .c_str() since AddQueryStringParameter takes a const char* key. private static String queryParamKeyExpression(Shape key, Model model) { if (CppTypeMapper.isEnum(key)) { String enumType = CppTypeMapper.getCppType(key, model, false); @@ -163,10 +155,9 @@ private static String queryParamKeyExpression(Shape key, Model model) { return "item.first.c_str()"; } - // Shared stream expression for a scalar member, list element, or map value: enum → Mapper - // lookup, timestamp → the query timestamp mapping (date-time→ISO_8601, http-date→RFC822, - // epoch-seconds→SecondsWithMSPrecision()), any other value streamed directly. The non-enum - // scalar path reuses this too (enum scalars are handled inline before reaching here). + // Shared stream expression for a scalar member, list element, or map value: enum → Mapper lookup, + // timestamp → query mapping (date-time→ISO_8601, http-date→RFC822, epoch-seconds→ + // SecondsWithMSPrecision()), else streamed directly. (Enum scalars are handled inline earlier.) private static String elementStreamExpression(String var, Shape target, Model model) { if (CppTypeMapper.isEnum(target)) { String enumType = CppTypeMapper.getCppType(target, model, false); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index f6c96708703..37ef5369a2b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -70,15 +70,14 @@ public void render(CppWriterDelegator writerDelegator) { private void renderHeader(CppWriterDelegator writerDelegator, StructureShape rawShape, OperationShape operation) { String className = operation.getId().getName() + "Request"; - // A request with a raw streaming @httpPayload member derives from StreamingRequest, - // whose base (AmazonStreamingWebServiceRequest) supplies GetBody/SetBody and - // GetContentType/SetContentType, and emits no SerializePayload. Matching C2J: contentType - // is stripped from the model entirely (affects includes AND rendering), while the payload - // member stays for include computation but is skipped in accessor/private rendering. + // A raw streaming @httpPayload request derives from StreamingRequest, whose base + // (AmazonStreamingWebServiceRequest) supplies GetBody/SetBody, GetContentType/SetContentType, + // and no SerializePayload. Matching C2J: contentType is stripped entirely (includes and + // rendering); the payload member stays for includes but is skipped in accessor/private rendering. boolean rawStreamingPayload = ShapeClassifier.isRawStreamingPayloadRequest(operation, ctx.model()); - // An event-stream (input) member targets a @streaming union: C2J renders it as a - // std::shared_ptr with a collision-renamed getter and an inline empty - // SerializePayload + GetBody() override, so it is excluded from the generic member path. + // An event-stream (input) member targets a @streaming union: rendered as a + // std::shared_ptr (collision-renamed getter, inline empty SerializePayload + + // GetBody() override), so excluded from the generic member path. Matches C2J. Optional eventStreamMember = ShapeClassifier.eventStreamMemberName(rawShape, ctx.model()); StructureShape includeShape = rawStreamingPayload ? shapeExcluding(rawShape, Set.of("contentType", "ContentType")) : rawShape; @@ -90,10 +89,9 @@ private void renderHeader(CppWriterDelegator writerDelegator, Set includes = new TreeSet<>(); includes.add(""); includes.add(""); - // NOTE: request headers do NOT include even when they - // declare URI-taking methods (DumpBodyToUrl / AddQueryStringParameters). The - // base AmazonWebServiceRequest.h forward-declares Aws::Http::URI, which is - // sufficient for a reference parameter, and C2J relies on that forward decl. + // Request headers do NOT include even with URI-taking methods + // (DumpBodyToUrl / AddQueryStringParameters): AmazonWebServiceRequest.h forward-declares + // Aws::Http::URI, sufficient for a reference param. Matches C2J. List memberIncludes = CppTypeMapper.getIncludesForShape(includeShape, ctx.model(), ctx.smithyServiceName()); includes.addAll(memberIncludes); IncludeSets.emitAngleIncludes(writer, includes); @@ -142,9 +140,8 @@ private void renderHeader(CppWriterDelegator writerDelegator, if (streamingRequest) { writer.write("inline virtual bool IsEventStreamRequest() const override { return true; }"); } - // Long-polling requests emit IsLongPollingOperation() -> true with the top identity - // methods (C2J RequestHeader.vm order: after IsEventStreamRequest, before - // HasEventStreamResponse); the marker is stamped by LongPollingTransform. + // Long-polling requests emit IsLongPollingOperation() -> true (C2J order: after + // IsEventStreamRequest, before HasEventStreamResponse); marker stamped by LongPollingTransform. if (shape.hasTrait(LongPollingTrait.class)) { writer.write("inline virtual bool IsLongPollingOperation() const override { return true; }"); } @@ -152,19 +149,18 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.write("inline virtual bool HasEventStreamResponse() const override { return true; }"); } if (eventStreamMember.isPresent()) { - // The request body is sent as an encoded event stream via GetBody(), so - // SerializePayload is an inline no-op (matches C2J RequestHeader.vm). + // Body is sent as an encoded event stream via GetBody(), so SerializePayload is an + // inline no-op. Matches C2J RequestHeader.vm. writer.write("// SerializePayload will not be invoked."); writer.write("// This request is sent by encoding its data in event-streams which is sent as IOStream via GetBody()"); writer.write("$L Aws::String SerializePayload() const override { return {}; }", ctx.exportMacro()); writer.write("$L std::shared_ptr GetBody() const override;", ctx.exportMacro()); } ctx.protocolTraits().writeRequestMethodDecls(writer, ctx.exportMacro(), shape, operation, ctx.model()); - // DumpBodyToUrl is emitted protocol-agnostically (C2J RequestHeader.vm gates it only on - // $shape.supportsPresigning). It is a protected virtual in AmazonWebServiceRequest, so the - // override is bracketed under protected: and the section restored to public: afterwards. - // The trait is stamped on the OPERATION (SupportsPresigningTransform) so Unit-input ops - // are covered and the decl stays symmetric with the protocol-emitted impl. + // DumpBodyToUrl is emitted protocol-agnostically (C2J gates only on + // $shape.supportsPresigning). A protected virtual, so bracketed under protected: then + // restored to public:. The trait is on the OPERATION (SupportsPresigningTransform) to + // cover Unit-input ops and stay symmetric with the protocol-emitted impl. if (operation.hasTrait(SupportsPresigningTrait.class)) { writer.write(""); writer.dedent(); @@ -182,9 +178,8 @@ private void renderHeader(CppWriterDelegator writerDelegator, renderContentMd5Decl(writer, operation); renderSignBodyDecl(writer, shape, operation); - // Chunked-encoding requests emit IsChunked() -> true right after SignBody and before - // IsStreaming (C2J RequestHeader.vm order); the marker is stamped by - // ChunkedEncodingTransform. + // Chunked-encoding requests emit IsChunked() -> true after SignBody, before IsStreaming + // (C2J order); marker stamped by ChunkedEncodingTransform. if (shape.hasTrait(ChunkedEncodingTrait.class)) { writer.write("$L bool IsChunked() const override { return true; }", ctx.exportMacro()); } @@ -278,18 +273,16 @@ private void renderSource(CppWriterDelegator writerDelegator, String className = operation.getId().getName() + "Request"; Optional eventStreamMember = ShapeClassifier.eventStreamMemberName(rawShape, ctx.model()); // Render request-method impls from the same member-stripped shape as the header (see - // renderedShape): the declaration and definition gate GetRequestSpecificHeaders / - // AddQueryStringParameters on this shape, so they must agree or the source emits an - // out-of-line definition of a method the class never declares. + // renderedShape): decl and def gate GetRequestSpecificHeaders / AddQueryStringParameters on + // this shape, so they must agree or the source defines a method the class never declares. StructureShape shape = renderedShape(rawShape, operation); String fileName = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(fileName, writer -> { - // A request with stream members (a raw @httpPayload blob/string body, or an event-stream - // input) is sent via the request body stream, not a serialized JSON payload. C2J routes - // it through StreamRequestSource.vm: AmazonWebServiceResult.h + the Stream/Utils/Aws - // usings (not the JSON serde header/usings). The event-stream sub-case also defines - // GetBody() returning its encoder member. + // A request with stream members (raw @httpPayload body or event-stream input) is sent via + // the body stream, not a serialized payload. C2J routes it through StreamRequestSource.vm: + // AmazonWebServiceResult.h + Stream/Utils/Aws usings (not the JSON serde header/usings). + // The event-stream sub-case also defines GetBody() returning its encoder member. boolean streaming = ShapeClassifier.isRawStreamingPayloadRequest(operation, ctx.model()) || eventStreamMember.isPresent(); if (streaming) { @@ -297,10 +290,10 @@ private void renderSource(CppWriterDelegator writerDelegator, IncludeSets.requestSourceBase(ctx.smithyServiceName(), className)); includes.add("aws/core/AmazonWebServiceResult.h"); includes.add("utility"); - // A raw-streaming-payload request sends its body via the streaming base class (no - // protocol serde), but its header/query members still serialize, so the source - // needs HashingUtils.h (blob Base64) and AWSStringStream.h unconditionally. It must - // NOT pull the protocol serde header (e.g. JsonSerializer.h), matching C2J. + // A raw-streaming-payload request sends its body via the streaming base (no protocol + // serde), but header/query members still serialize, so the source needs HashingUtils.h + // (blob Base64) and AWSStringStream.h unconditionally, and must NOT pull the protocol + // serde header (e.g. JsonSerializer.h). Matches C2J. includes.add("aws/core/utils/HashingUtils.h"); includes.add("aws/core/utils/memory/stl/AWSStringStream.h"); IncludeSets.emit(writer, includes); @@ -356,11 +349,9 @@ private void renderSource(CppWriterDelegator writerDelegator, /** * The shape whose members drive request-method rendering. Raw-streaming-payload requests strip - * {@code contentType} (supplied by the streaming base's GetContentType/SetContentType) and the - * {@code @httpPayload} member (sent via the body stream); an event-stream input member is - * rendered separately, not through the generic member path. Both the header (declarations) and - * the source (definitions) MUST render from this same shape so their emitted method sets stay - * in sync. + * {@code contentType} (from the streaming base) and the {@code @httpPayload} member (sent via + * the body stream); an event-stream input member is rendered separately. Header and source MUST + * render from this same shape so their emitted method sets stay in sync. */ private StructureShape renderedShape(StructureShape rawShape, OperationShape operation) { Set excluded = new HashSet<>(); @@ -399,8 +390,8 @@ private String eventStreamUnionType(StructureShape shape, String memberName) { /** * Renders the accessor block for an event-stream (input) member: a {@code std::shared_ptr} - * getter/setter/wither. The getter is renamed to {@code GetMember} because {@code GetBody} - * is reserved by the streaming request base (matches C2J's collision handling). + * getter/setter/wither. The getter is renamed to {@code GetMember} since {@code GetBody} is + * reserved by the streaming request base (C2J collision handling). */ private void renderEventStreamMemberAccessor(CppWriter writer, String className, StructureShape shape, String memberName) { @@ -429,15 +420,14 @@ private void renderEventStreamMemberAccessor(CppWriter writer, String className, } /** - * Declares the request methods for an operation carrying {@code @httpChecksum}, gated per - * sub-field, matching C2J's RequestHeader.vm:89-104: + * Declares the {@code @httpChecksum} request methods, gated per sub-field (C2J RequestHeader.vm): *

    *
  • {@code requestAlgorithmMember} → {@code GetChecksumAlgorithmName} / {@code ChecksumAlgorithmIsSet}
  • *
  • {@code requestValidationModeMember} → {@code ShouldValidateResponseChecksum}
  • *
  • {@code requestChecksumRequired} → inline {@code RequestChecksumRequired}
  • *
  • {@code responseAlgorithms} → {@code GetResponseChecksumAlgorithmNames}
  • *
- * All override base {@code AmazonWebServiceRequest} virtuals; no extra includes are needed. + * All override base {@code AmazonWebServiceRequest} virtuals; no extra includes. */ private void renderChecksumDecls(CppWriter writer, StructureShape shape, OperationShape operation) { Optional maybeTrait = operation.getTrait(HttpChecksumTrait.class); @@ -461,10 +451,10 @@ private void renderChecksumDecls(CppWriter writer, StructureShape shape, Operati } /** - * Defines the {@code @httpChecksum} request methods, matching C2J's ModelClassChecksumMembers.vm. + * Defines the {@code @httpChecksum} request methods (C2J ModelClassChecksumMembers.vm). * {@code GetChecksumAlgorithmName} defaults to {@code "crc64nvme"} when the algorithm member is - * unset, else maps the enum via its generated Mapper. The bodies read only the request's own - * enum members, so they are independent of the (stubbed) payload serde. + * unset, else maps the enum via its Mapper. Bodies read only the request's enum members, so they + * are independent of the (stubbed) payload serde. */ private void renderChecksumImpls(CppWriter writer, String className, StructureShape shape, OperationShape operation) { @@ -508,11 +498,10 @@ private void renderChecksumImpls(CppWriter writer, String className, StructureSh } /** - * Declares the inline {@code ShouldComputeContentMd5} override for an operation carrying the - * legacy {@code @httpChecksumRequired} trait ({@code smithy.api#httpChecksumRequired}), which - * requests a {@code Content-MD5} header. Distinct from the flexible {@code @httpChecksum} trait. - * C2J derives an internal {@code Shape.computeContentMd5} flag from it; here it reads the trait - * directly. Matches RequestHeader.vm:105-108 (no {@code .cpp} body). + * Declares the inline {@code ShouldComputeContentMd5} override for the legacy + * {@code @httpChecksumRequired} trait (requests a {@code Content-MD5} header; distinct from the + * flexible {@code @httpChecksum}). Read directly here rather than via C2J's derived + * {@code computeContentMd5} flag. Matches RequestHeader.vm (no {@code .cpp} body). */ private void renderContentMd5Decl(CppWriter writer, OperationShape operation) { if (operation.hasTrait(HttpChecksumRequiredTrait.class)) { @@ -522,12 +511,10 @@ private void renderContentMd5Decl(CppWriter writer, OperationShape operation) { } /** - * Declares the inline {@code SignBody} override for an operation carrying - * {@code aws.auth#unsignedPayload} whose request has at least one member. C2J's - * RequestHeader.vm emits this for a {@code v4-unsigned-body} request - * ({@code #if(!$shape.signBody && $shape.members.size() > 0)}); the Smithy equivalent is the - * {@code @unsignedPayload} operation trait. This closes {@code Model::}-namespace parity only: - * the Smithy runtime does not consume {@code SignBody()} (it hardcodes signing). + * Declares the inline {@code SignBody} override for an {@code @unsignedPayload} operation whose + * request has at least one member (C2J's {@code v4-unsigned-body} case). Closes + * {@code Model::}-namespace parity only: the Smithy runtime hardcodes signing and never consumes + * {@code SignBody()}. */ private void renderSignBodyDecl(CppWriter writer, StructureShape shape, OperationShape operation) { if (operation.hasTrait(UnsignedPayloadTrait.class) && !shape.getAllMembers().isEmpty()) { @@ -546,11 +533,9 @@ private String checksumMemberEnumType(StructureShape shape, OperationShape opera } /** - * Declares {@code GetSelectedCompressionAlgorithm} for an operation carrying - * {@code @requestCompression}. The method overrides a base {@code AmazonWebServiceRequest} - * virtual, so no extra include is needed. Only gzip is supported (validated), so the - * declaration is always guarded by {@code ENABLED_ZLIB_REQUEST_COMPRESSION}. Matches C2J's - * {@code RequestHeader.vm}. + * Declares {@code GetSelectedCompressionAlgorithm} for a {@code @requestCompression} operation. + * Overrides a base virtual (no extra include). Only gzip is supported (validated), so the decl + * is guarded by {@code ENABLED_ZLIB_REQUEST_COMPRESSION}. Matches C2J {@code RequestHeader.vm}. */ private void renderRequestCompressionDecl(CppWriter writer, OperationShape operation) { if (!operation.hasTrait(RequestCompressionTrait.class)) { @@ -564,11 +549,10 @@ private void renderRequestCompressionDecl(CppWriter writer, OperationShape opera } /** - * Defines {@code GetSelectedCompressionAlgorithm}. Streaming requests can't size their body up - * front, so they compress whenever enabled; non-streaming requests skip compression below the - * configured minimum body size. Matches C2J's ModelClassRequiredCompression[Stream].vm. This - * body only reads the already-serialized body via the base {@code GetBody()}, so it is - * independent of the (currently stubbed) payload serde. + * Defines {@code GetSelectedCompressionAlgorithm}: streaming requests can't size their body up + * front so they compress whenever enabled; non-streaming requests skip below the configured + * minimum body size. Matches C2J ModelClassRequiredCompression[Stream].vm. Reads only the + * already-serialized body via the base {@code GetBody()}, so it is independent of payload serde. */ private void renderRequestCompressionImpl(CppWriter writer, String className, OperationShape operation, boolean streaming) { @@ -599,9 +583,8 @@ private void renderRequestCompressionImpl(CppWriter writer, String className, } /** - * Enforces the C2J contract that {@code @requestCompression} declares exactly the gzip encoding - * (the only algorithm the SDK supports). Fails fast on an empty or unsupported encoding list, - * mirroring the legacy C2J transformer. + * Enforces the C2J contract that {@code @requestCompression} declares exactly gzip (the only + * algorithm the SDK supports). Fails fast on an empty or unsupported encoding list. */ private static void validateGzipEncoding(OperationShape operation) { List encodings = operation.expectTrait(RequestCompressionTrait.class).getEncodings(); @@ -689,9 +672,8 @@ private void renderOperationContextParamsAccessor(CppWriter writer, String class writer.openBlock("Aws::Vector $L::GetOperationContextParams() const {", "}", className, () -> { writer.write("Aws::Vector result;"); - // Visitor output is a single newline-separated string of flat (un-indented) statements; - // CppWriter applies the block indentation and clang-format normalizes the rest. Each line - // is passed as a $L argument so any $L or {n} tokens in the output are not reinterpreted. + // Visitor output is a newline-separated string of flat statements; CppWriter indents and + // clang-format normalizes. Each line is passed as a $L arg so $L/{n} tokens aren't reinterpreted. String raw = emit.statements(); if (!raw.isEmpty()) { // Trim the single trailing newline the visitor always emits so we don't double-blank. diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java index af8b44a3006..930582bcb95 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java @@ -50,9 +50,7 @@ public void render(CppWriterDelegator writerDelegator) { } /** - * The name of the {@code @httpPayload} streaming member. Only called for results the - * classifier already flagged as streaming, so a missing payload member is a codegen bug - * rather than a modeled state — fail fast instead of returning null. + * Name of the {@code @httpPayload} streaming member; a missing member is a codegen bug, so fail fast. */ private String streamingPayloadMemberName(StructureShape shape) { for (Map.Entry entry : shape.getAllMembers().entrySet()) { @@ -71,15 +69,13 @@ private void renderHeader(CppWriterDelegator writerDelegator, writerDelegator.useFileWriter(fileName, writer -> { writer.write("#pragma once"); - // AWSString.h is only needed for the top-level m_requestId; string-typed members - // bring their own include via getIncludesForShape. Matches C2J include hygiene. + // AWSString.h only for top-level m_requestId; string members self-include. Matches C2J. List includes = new java.util.ArrayList<>(IncludeSets.resultHeaderBase( ctx.smithyServiceName(), ctx.namespace(), ctx.protocolTraits().resultHasTopLevelRequestId())); for (String memberInc : CppTypeMapper.getIncludesForShape(shape, ctx.model(), ctx.smithyServiceName())) { includes.add(memberInc); } - // Protocols whose result-header serde types are named in the class signature (CBOR: - // CborValue) add their own header here; JSON/XML forward-declare and add nothing. + // Protocols naming serde types in the class signature (CBOR: CborValue) add their header; JSON/XML forward-declare. includes.addAll(ctx.protocolTraits().serdeIncludes(FileKind.RESULT_HEADER)); IncludeSets.emitAngleIncludes(writer, includes); @@ -115,8 +111,7 @@ private void renderHeader(CppWriterDelegator writerDelegator, MemberRenderer.renderRequestIdAccessors(writer, className); } - // The top-level HostId (x-amz-id-2) group is per-service (S3 Control only), driven - // by the internal marker rather than a protocol flag, and always follows RequestId. + // Top-level HostId (x-amz-id-2), S3 Control only, driven by the internal marker (not a protocol flag); always follows RequestId. boolean topLevelHostId = shape.hasTrait(TopLevelHostIdTrait.class); if (topLevelHostId) { MemberRenderer.renderHostIdAccessors(writer, className); @@ -130,8 +125,7 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.indent(); members.renderDataMembers(writer); if (topLevelRequestId) { - // The blank line separates the modeled members from the m_requestId group; - // C2J omits it (and m_requestId) for Query/EC2 results. + // Blank line separates modeled members from the m_requestId group; C2J omits both for Query/EC2. writer.write(""); writer.write("Aws::String m_requestId;"); } @@ -175,9 +169,8 @@ private void renderSource(CppWriterDelegator writerDelegator, } /** - * Renders a streaming result header: a move-only class whose payload is an - * {@code Aws::Utils::Stream::ResponseStream} exposed via {@code GetBody()} / - * {@code ReplaceBody}, matching the legacy C2J {@code StreamResultHeader.vm} output. + * Renders a streaming result header: a move-only class whose ResponseStream payload is exposed + * via {@code GetBody()} / {@code ReplaceBody}. Matches C2J {@code StreamResultHeader.vm}. */ private void renderStreamingHeader(CppWriterDelegator writerDelegator, StructureShape shape, OperationShape operation) { @@ -226,9 +219,8 @@ private void renderStreamingHeader(CppWriterDelegator writerDelegator, ctx.exportMacro(), className); writer.write(""); - // Streaming payload accessors (no Set/With/HasBeenSet for the stream member). - // The getter is named after the member (GetBody, GetAudioStream, GetResponse); - // ReplaceBody stays literal, matching C2J StreamResultHeader.vm. + // Streaming payload accessors (no Set/With/HasBeenSet). Getter named after the member; + // ReplaceBody stays literal. Matches C2J StreamResultHeader.vm. String streamField = CppNames.fieldName(streamMember); writer.write("///@{"); shape.getMember(streamMember) @@ -248,8 +240,7 @@ private void renderStreamingHeader(CppWriterDelegator writerDelegator, MemberRenderer.renderRequestIdAccessors(writer, className); - // S3 Control has no streaming results today; the marker is only stamped on its - // outputs, so this block is a defensive no-op for every current streaming result. + // Defensive no-op: no streaming result is S3 Control today, so the marker is never stamped here. boolean topLevelHostId = shape.hasTrait(TopLevelHostIdTrait.class); if (topLevelHostId) { MemberRenderer.renderHostIdAccessors(writer, className); @@ -283,8 +274,8 @@ private void renderStreamingHeader(CppWriterDelegator writerDelegator, } /** - * Renders a streaming result source: the move ctor/assign take ownership of the - * response payload stream ({@code TakeOwnershipOfPayload}) rather than parsing a body. + * Renders a streaming result source: move ctor/assign take ownership of the payload stream + * ({@code TakeOwnershipOfPayload}) instead of parsing a body. */ private void renderStreamingSource(CppWriterDelegator writerDelegator, StructureShape shape, OperationShape operation) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java index 7a6dfdbf970..477a253f680 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java @@ -41,10 +41,9 @@ public SubObjectRenderer(List subObjects, RenderContext ctx) { @Override public void render(CppWriterDelegator writerDelegator) { for (Shape shape : subObjects) { - // C2J models a union as a structure with "union": true and emits it through the same - // ModelClass templates, so structures and (non-streaming) unions render identically. - // @streaming unions are the event-stream shapes rendered by EventStreamRenderer / - // the outgoing-event-stream path; skip them here to avoid a double-write. + // C2J renders a (non-streaming) union like a structure via the same ModelClass + // templates, so they render identically here. @streaming unions are event-stream shapes + // handled elsewhere; skip them to avoid a double-write. boolean isStruct = shape.isStructureShape(); boolean isDataUnion = shape.isUnionShape() && !shape.hasTrait(StreamingTrait.class); if (isStruct || isDataUnion) { @@ -57,20 +56,17 @@ public void render(CppWriterDelegator writerDelegator) { private void renderHeader(CppWriterDelegator writerDelegator, Shape shape) { String className = CppTypeMapper.cppShapeName(shape); String fileName = "include/aws/" + ctx.smithyServiceName() + "/model/" + className + ".h"; - // A shape that is BOTH an operation output AND referenced as a member ("dual-role") is - // stamped with the top-level requestId by C2J — but only for JSON-family protocols. Query/EC2 - // instead inject a ResponseMetadata member (GlobalTransforms.injectResponseMetadata), so they - // are gated out here via resultHasTopLevelRequestId(). + // A "dual-role" shape (both an operation output and a referenced member) gets the top-level + // requestId from C2J, but only for JSON-family protocols. Query/EC2 inject ResponseMetadata + // instead, so they are gated out via resultHasTopLevelRequestId(). boolean stampRequestId = resultOutputIds.contains(shape.getId()) && ctx.protocolTraits().resultHasTopLevelRequestId(); writerDelegator.useFileWriter(fileName, writer -> { writer.write("#pragma once"); - // Includes List includes = new java.util.ArrayList<>(); includes.add("aws/" + ctx.smithyServiceName() + "/" + ctx.namespace() + "_EXPORTS.h"); if (stampRequestId) { - // The stamped m_requestId is an Aws::String; mirror the result-header include hygiene. includes.add("aws/core/utils/memory/stl/AWSString.h"); } for (String memberInc : CppTypeMapper.getIncludesForShape(shape, ctx.model(), ctx.smithyServiceName())) { @@ -91,8 +87,8 @@ private void renderHeader(CppWriterDelegator writerDelegator, Shape shape) { ModelFile.modelNamespace(writer, ctx.namespace(), () -> ctx.protocolTraits().writeShapeForwardDeclarations(writer), () -> { - // Recursive member targets are forward-declared here (at Model scope) instead of - // included, breaking the reference cycle. Matches C2J's computeForwardDeclarations. + // Recursive member targets are forward-declared at Model scope (not included) to break + // the cycle. Matches C2J computeForwardDeclarations. for (String fwd : CppTypeMapper.getForwardDeclarations(shape, ctx.model())) { writer.write("class $L;", fwd); } @@ -103,10 +99,9 @@ private void renderHeader(CppWriterDelegator writerDelegator, Shape shape) { writer.openBlock("class $L {", "};", className, () -> { writer.write("public:"); ctx.protocolTraits().writeSerdeMethodDecls(writer, ctx.exportMacro(), className, null); - // A memberless shape ends right after its serde decls: C2J emits no accessors - // and no private: section (ModelClassMembersAndInlines.vm gates both on - // $shape.members.size() > 0) — unless it is a dual-role output, in which case the - // stamped requestId group still needs a private: section. + // A memberless shape ends right after its serde decls (no accessors, no private:), + // matching C2J — unless it is a dual-role output, whose stamped requestId group + // still needs a private: section. boolean hasMembers = !shape.getAllMembers().isEmpty(); if (hasMembers || stampRequestId) { MemberRenderer members = MemberRenderer.forStructure(ctx.model(), shape, className) @@ -116,9 +111,8 @@ private void renderHeader(CppWriterDelegator writerDelegator, Shape shape) { members.renderPublicAccessors(writer); } if (stampRequestId) { - // MODEL-class requestId group (includes the RequestIdHasBeenSet() getter), - // emitted after the modeled-member accessors. The helper writes its own - // leading blank-line separator. + // MODEL-class requestId group (with RequestIdHasBeenSet() getter), after the + // modeled accessors. The helper writes its own leading blank-line separator. MemberRenderer.renderRequestIdAccessors(writer, className, true); } writer.dedent(); @@ -128,8 +122,8 @@ private void renderHeader(CppWriterDelegator writerDelegator, Shape shape) { members.renderDataMembers(writer); } if (stampRequestId) { - // m_requestId trails the modeled data members (blank-line separated, matching - // MemberRenderer's data-member spacing); its flag trails the modeled flags. + // m_requestId trails the modeled data members (blank-line separated like + // MemberRenderer's spacing); its flag trails the modeled flags. if (hasMembers) { writer.write(""); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java index bc141d2142b..8889498c04c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitor.java @@ -22,12 +22,10 @@ import software.amazon.smithy.model.shapes.StructureShape; /** - * Translates the JMESPath expression carried by {@code smithy.rules#operationContextParams} - * into C++ that walks the request struct and pushes leaf values into a {@code result} - * {@code Aws::Vector}. Immutable and value-returning: each visit returns an - * {@link Emit}; parents compose children's results. Produces the same C++ structure as the - * legacy C2J generator (identifiers and statements), modulo whitespace (normalized downstream - * by clang-format). + * Translates the JMESPath expression from {@code smithy.rules#operationContextParams} into C++ that + * walks the request struct and pushes leaf values into a {@code result} {@code Aws::Vector}. + * Immutable and value-returning: each visit returns an {@link Emit}, parents compose children's + * results. Produces the same C++ as legacy C2J (identifiers/statements), modulo whitespace. */ public final class SmithyEndpointsJmesPathVisitor extends UnsupportedExpressionVisitor { @@ -87,9 +85,8 @@ public Emit visitSubexpression(Subexpression expression) { public Emit visitProjection(ProjectionExpression expression) { Emit left = expression.getLeft().accept(this); if (!(left.shape() instanceof ListShape)) { - // No list to iterate at this node (e.g. the trailing flatten-projection of a - // multi-select pattern, whose left subtree already emitted every statement). - // Propagate the left subtree's statements rather than discarding them. + // No list to iterate here (e.g. the trailing flatten-projection of a multi-select, whose + // left subtree already emitted every statement); propagate them rather than discarding. return left; } String alias = left.rootName() + "Elems"; diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java index 94f8150efb7..d29a00c398c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java @@ -19,17 +19,11 @@ import java.util.Optional; /** - * Access Analyzer model parity with the legacy C2J transformer, which resolves the collision - * between the {@code GetGeneratedPolicy} result wrapper and the domain shape - * {@code GeneratedPolicyResult} by renaming the domain shape (and its referencing member) to - * {@code GeneratedPolicyResults}. C2J preserves the wire key ({@code generatedPolicyResult}) via - * {@code locationName}; {@link TransformSupport#renameMember} mirrors that by pinning the original - * wire name through the service's protocol-appropriate trait, so the model stays serde-correct even - * though serde is currently stubbed. - * - *

Self-guards on the raw smithy service name {@code accessanalyzer} (transforms never remap). - * No-op when the domain shape is absent (upstream already clean). Throws if the target name - * {@code GeneratedPolicyResults} is already occupied by a distinct shape — a genuine collision. + * Access Analyzer C2J parity: resolves the collision between the GetGeneratedPolicy result wrapper + * and the domain shape GeneratedPolicyResult by renaming the domain shape (and its referencing + * member) to GeneratedPolicyResults, pinning the original wire key via the protocol-appropriate + * trait so serde stays correct. Self-guards on service name accessanalyzer; no-op when the domain + * shape is absent; throws if GeneratedPolicyResults is already occupied by a distinct shape. */ public final class AccessAnalyzerTransforms { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java index 5abe1607c67..bce200d44b5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AdditionalRequestHeadersTrait.java @@ -15,18 +15,10 @@ import java.util.Map; /** - * Internal marker (never declared in any model file) placed by {@link GlacierTransforms} on each - * request structure that C2J attaches constant request headers to via - * {@code metadata.setAdditionalHeaders(...)}. It carries the ordered header name → value pairs - * (for Glacier, {@code x-amz-glacier-version} → the service API version). - * - *

C2J emits these headers from the {@code Request} base class ({@code GetHeaders}) for - * ordinary requests, but a streaming request derives from {@code AmazonStreamingWebServiceRequest} - * and bypasses that base, so {@code StreamRequestSource.vm} instead emits them inside the request's - * own {@code GetRequestSpecificHeaders}. The base class stays C2J-generated, so only the streaming - * requests carry this marker; request rendering turns it into the matching {@code headers.insert(...)} - * lines. Kept as a data-carrying marker + generic renderer rule so the renderer stays - * service-agnostic. + * Internal marker placed by GlacierTransforms on request structures C2J attaches constant headers to + * via metadata.setAdditionalHeaders(...). Carries the ordered header name-value pairs (for Glacier, + * x-amz-glacier-version -> the API version). Only streaming requests carry it: they bypass the + * C2J-generated base GetHeaders, so request rendering emits the matching headers.insert(...) lines. */ public final class AdditionalRequestHeadersTrait extends AbstractTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#additionalRequestHeaders"); @@ -38,7 +30,6 @@ public AdditionalRequestHeadersTrait(Map headers) { this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); } - /** Ordered header name → value pairs, emitted verbatim into {@code GetRequestSpecificHeaders}. */ public Map getHeaders() { return headers; } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java index a0bd440e336..d791d924120 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChecksumMemberTrait.java @@ -9,14 +9,9 @@ import software.amazon.smithy.model.traits.StringTrait; /** - * Internal marker (never declared in any model file) placed by {@link S3Transforms} on each S3 request - * member that C2J's {@code S3RestXmlCppClientGenerator} flags via {@code member.setChecksumMember(true)} - * + {@code member.setChecksumEnumMember(...)} — the {@code ChecksumCRC32}/{@code ChecksumSHA256}/etc. - * members of any request that also carries a {@code ChecksumAlgorithm} member. The stored value is the - * matching {@code ChecksumAlgorithm} enum constant (e.g. {@code CRC32}). Member rendering turns the - * marker into the C2J {@code ModelClassMembersAndInlines.vm} behavior: each setter also calls - * {@code SetChecksumAlgorithm(ChecksumAlgorithm::)}, plus a {@code const char*} overload that does - * the same. Kept as a marker + generic renderer rule so the member renderer stays service-agnostic. + * Internal marker on S3 request checksum members (C2J setChecksumMember/setChecksumEnumMember). Stores + * the matching ChecksumAlgorithm enum constant; member rendering makes each setter also call + * SetChecksumAlgorithm(ChecksumAlgorithm::) (plus a const char* overload). */ public final class ChecksumMemberTrait extends StringTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#checksumMember"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java index db7744e5e2b..2f587d666db 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTrait.java @@ -9,15 +9,10 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal marker (never declared in any model file) placed by {@link ChunkedEncodingTransform} on - * each request structure for which C2J's {@code RequestHeader.vm} emits - * {@code bool IsChunked() const override { return true; }}. C2J gates that override on - * {@code ($metadata.serviceId=="MediaStore Data" || $operation.supportsChunkedEncoding)} together - * with {@code $shape.hasStreamMembers() && !$shape.signBody && $shape.members.size() > 0}; S3 sets - * {@code supportsChunkedEncoding} on {@code WriteGetObjectResponse} only. The transform collapses - * that emit-time condition into a single stamping decision so request rendering only has to turn the - * marker into the override. Kept as a marker + generic renderer rule (not a service-name {@code if}) - * so the renderer stays service-agnostic. + * Internal marker placed by ChunkedEncodingTransform on request structures for which C2J emits + * {@code bool IsChunked() const override { return true; }}. Collapses C2J's emit-time gate + * (MediaStore Data or supportsChunkedEncoding, plus streaming members and !signBody) into a single + * stamping decision; request rendering turns the marker into the override. */ public final class ChunkedEncodingTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#chunkedEncoding"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java index 2529fcc6dff..9f8683c447c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java @@ -18,16 +18,11 @@ import java.util.List; /** - * Stamps the internal {@link ChunkedEncodingTrait} onto the request structures for which C2J's - * {@code RequestHeader.vm} emits {@code bool IsChunked() const override { return true; }}. C2J gates - * that override on {@code ($metadata.serviceId=="MediaStore Data" || $operation.supportsChunkedEncoding)} - * (S3 sets {@code supportsChunkedEncoding} on {@code WriteGetObjectResponse} only) combined with - * {@code $shape.hasStreamMembers() && !$shape.signBody && $shape.members.size() > 0}. This transform - * collapses that emit-time condition into a single stamping decision: an operation qualifies when it - * carries {@code aws.auth#unsignedPayload} (the {@code !signBody} proxy), its input is a raw - * streaming payload request (the {@code hasStreamMembers} proxy, which also guarantees members > 0), - * and either the service is MediaStore Data or the operation is S3's {@code WriteGetObjectResponse}. - * No-op for any other service/operation, leaving the model instance untouched. + * Stamps {@link ChunkedEncodingTrait} onto request structures for which C2J emits + * {@code bool IsChunked() const override { return true; }}. An operation qualifies when it carries + * aws.auth#unsignedPayload (the !signBody proxy), its input is a raw streaming payload request (the + * hasStreamMembers proxy, also guaranteeing members > 0), and either the service is MediaStore Data + * or the operation is S3's WriteGetObjectResponse. No-op otherwise. */ public final class ChunkedEncodingTransform { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java index 34e6187d997..1cbcf123c46 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomRenderedTrait.java @@ -9,23 +9,14 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal, synthetic marker trait stamped onto a shape by a service-level transform to signal that - * the shape is emitted by a dedicated {@code ShapeRenderer} (e.g. {@code DynamoDbRenderer}) rather - * than by the generic {@code SubObjectRenderer}. {@code ShapeClassifier} skips any structure/union - * bearing this trait, so the default sub-object body is never emitted for it — preventing the - * double-emit that {@code CppWriterDelegator}'s append-on-existing-writer behaviour would otherwise - * silently produce. - * - *

This trait is never declared in a Smithy model file; it exists only as an in-memory trait - * instance added inside a {@code ModelTransformer}. Smithy does not require a model-level trait - * definition for an in-memory instance because trait-definition validation runs only through the - * {@code ModelAssembler}, not through {@code Model.toBuilder().build()} / {@code ModelTransformer}. - * The synthetic {@code aws.cpp.internal} namespace keeps its id from ever colliding with a real - * modeled trait. + * Internal synthetic marker signaling that a shape is emitted by a dedicated ShapeRenderer (e.g. + * DynamoDbRenderer) rather than the generic SubObjectRenderer. ShapeClassifier skips any + * structure/union bearing it, preventing the double-emit that CppWriterDelegator's append-on-existing + * behaviour would otherwise produce. Exists only as an in-memory trait added inside a + * ModelTransformer; the aws.cpp.internal namespace keeps its id from colliding with a modeled trait. */ public final class CustomRenderedTrait extends AnnotationTrait { - /** The synthetic, internal-only id for this marker trait. */ public static final ShapeId ID = ShapeId.from("aws.cpp.internal#customRendered"); public CustomRenderedTrait() { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java index cb40c23c71e..923fcdd3d52 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/CustomizedAccessLogTagTrait.java @@ -9,15 +9,11 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal marker (never declared in any model file) placed by {@link S3Transforms} on the - * {@code customizedAccessLogTag} map member it injects onto every S3 request. C2J models that - * member with a distinct {@code customizedQuery} flag rather than an ordinary query-string map: - * its {@code AddQueryStringParametersToRequest.vm} skips the normal {@code @httpQueryParams} loop - * for it and instead emits the {@code x-}-prefix filter block ({@code collectedLogTags}). This - * marker preserves that distinction — {@code RequestQuerySerializer} skips the marked member in - * the normal map loop and emits the {@code x-} filter block for it after the loop. The member - * keeps its {@code @httpQueryParams} trait so the request still declares - * {@code AddQueryStringParameters} ({@code RequestBindings.hasQueryStringMembers}). + * Internal marker placed by S3Transforms on the customizedAccessLogTag map member injected onto every + * S3 request. C2J models it with a distinct customizedQuery flag rather than an ordinary query-string + * map; the marker makes RequestQuerySerializer skip it in the normal map loop and instead emit the + * x--prefix filter block after the loop. The member keeps @httpQueryParams so the request still + * declares AddQueryStringParameters. */ public final class CustomizedAccessLogTagTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#customizedAccessLogTag"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java index e4be2f81216..6b7c56ffc45 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java @@ -14,20 +14,12 @@ import java.util.Optional; /** - * DynamoDB model transform: marks the {@code AttributeValue} shape with {@link CustomRenderedTrait} - * so {@code ShapeClassifier} drops it from the default sub-object set. DynamoDB's {@code - * AttributeValue} is a bespoke document type emitted verbatim by {@code DynamoDbRenderer} (matching - * the legacy C2J {@code DynamoDBJsonCppClientGenerator}); if the generic {@code SubObjectRenderer} - * also emitted it, both writers would resolve the same {@code AttributeValue.h} path and append, - * silently concatenating the generic tagged-union struct onto the hand-written document type. - * - *

Keeping the suppression here — rather than as a service-name {@code if} in the generic - * {@code ModelGenerator} — keeps the orchestrator service-agnostic: the marker drives a generic - * classifier rule that applies to any shape a dedicated renderer owns. The shape itself stays in - * the model so member references (e.g. {@code PutItemInput.Item}) still resolve. - * - *

Self-guards on the raw smithy service name {@code dynamodb} (no-op for every other service). - * No-op when the {@code AttributeValue} shape is absent (upstream model changed). + * DynamoDB C2J parity: marks the AttributeValue shape with {@link CustomRenderedTrait} so + * ShapeClassifier drops it from the default sub-object set. AttributeValue is a bespoke document type + * emitted verbatim by DynamoDbRenderer; without this the generic SubObjectRenderer would resolve the + * same AttributeValue.h path and append, concatenating a generic tagged-union struct onto it. The + * shape stays in the model so member references still resolve. Self-guards on service name dynamodb; + * no-op when AttributeValue is absent. */ public final class DynamoDbTransforms { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java index c15df915a39..dc43824d1cf 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java @@ -21,25 +21,12 @@ import java.util.Optional; /** - * EC2 model parity with the legacy C2J {@code Ec2CppClientGenerator}: adds the unmodeled - * {@code disabled} value to {@code SpotInstanceState}, renames every {@code *Result} - * structure shape to {@code *Response} so nested domain structs (e.g. {@code MetricDataResult}) - * match C2J, and models {@code ModifyInstanceAttributeRequest.UserData} as the sensitive - * {@code SecureBlobAttributeValue} to match the C2J model. Operation-OUTPUT result files are - * handled centrally by {@code ShapeUtil.getResultSuffix}, but nested domain structs are rendered - * from the shape name by {@code SubObjectRenderer}, so those require a model-shape rename. Out of - * scope (client/endpoint path, left to C2J): the legacy error-code injection, CopySnapshot - * pre-signing, and endpoint template. - * - *

UserData / SecureBlobAttributeValue: the upstream {@code aws/aws-models} C2J model - * ({@code ec2//service-2.json}) marks {@code UserData} sensitive via - * {@code SecureBlobAttributeValue -> SecureBlob (@sensitive)}, but the upstream Smithy model - * ({@code ec2/smithy/model.json}) still targets the non-sensitive {@code BlobAttributeValue}. This - * transform mirrors the C2J modeling in the Smithy model so generated code matches. It is a - * temporary compensation for that upstream data lag; once the upstream Smithy model catches up and - * already defines {@code SecureBlobAttributeValue}, this transform throws {@code IllegalStateException} - * so a human removes it rather than letting it silently self-retire — see - * docs/superpowers/plans/parity-deltas.md. + * EC2 C2J parity: adds the unmodeled {@code disabled} value to SpotInstanceState; renames every + * {@code *Result} structure to {@code *Response} so nested domain structs (rendered from shape name + * via SubObjectRenderer) match C2J; and retargets ModifyInstanceAttributeRequest.UserData to the + * sensitive SecureBlobAttributeValue (upstream Smithy still targets the non-sensitive + * BlobAttributeValue). Out of scope (left to C2J): legacy error-code injection, CopySnapshot + * pre-signing, and the endpoint template. */ public final class Ec2Transforms { @@ -58,17 +45,11 @@ private static Model apply(Model model, ServiceShape service) { } /** - * Models {@code ModifyInstanceAttributeRequest.UserData} as {@code SecureBlobAttributeValue} - * (whose {@code Value} member targets a {@code @sensitive} {@code SecureBlob} blob), matching - * the C2J model. The upstream Smithy model still targets the non-sensitive - * {@code BlobAttributeValue}; after repointing, {@code BlobAttributeValue} is no longer - * referenced and drops out of the reachable (emitted) set, exactly as it does in C2J. - * - *

Throws {@code IllegalStateException} when {@code SecureBlobAttributeValue} already exists - * (upstream Smithy caught up), signalling this compensating transform is obsolete and must be - * removed. No-op — leaving the model untouched — when {@code ModifyInstanceAttributeRequest} or - * its {@code UserData} member is absent, or {@code UserData} no longer targets - * {@code BlobAttributeValue} (source-absent, not a collision). + * Models ModifyInstanceAttributeRequest.UserData as SecureBlobAttributeValue (whose Value member + * targets a @sensitive SecureBlob blob), matching C2J; the now-unreferenced BlobAttributeValue + * drops out of the emitted set. Throws when SecureBlobAttributeValue already exists (upstream + * Smithy caught up, so this transform is obsolete). No-op when ModifyInstanceAttributeRequest or + * its UserData member is absent, or UserData no longer targets BlobAttributeValue. */ private static Model addSecureBlobUserData(Model model) { Optional requestOpt = model.shapes(StructureShape.class) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java index 51fba711240..1165452bf05 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/EmbeddedErrorsTrait.java @@ -9,13 +9,9 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal marker (never declared in any model file) placed by {@link S3Transforms} on each S3 - * request structure that C2J's {@code S3RestXmlCppClientGenerator} lists in its hardcoded - * {@code functionsWithEmbeddedErrors} set ({@code shape.setEmbeddedErrors(true)}). REST-XML request - * rendering turns the marker into the {@code HasEmbeddedError(IOStream&, HeaderValueCollection&)} - * override that {@code RequestHeader.vm} emits under {@code #if($shape.hasEmbeddedErrors())}, so - * these S3 requests match C2J. Kept as a marker + generic renderer rule (not a service-name - * {@code if}) so the renderer stays service-agnostic; only S3 requests ever carry it. + * Internal marker placed by S3Transforms on S3 request structures in C2J's hardcoded + * functionsWithEmbeddedErrors set (shape.setEmbeddedErrors(true)). REST-XML request rendering turns + * the marker into the HasEmbeddedError(IOStream&, HeaderValueCollection&) override, matching C2J. */ public final class EmbeddedErrorsTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#embeddedErrors"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java index bab9d7fe54f..58aa21d8987 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java @@ -27,16 +27,11 @@ import java.util.stream.Collectors; /** - * Glacier parity with the legacy C2J {@code GlacierRestJsonCppClientGenerator} for the - * {@code Model::} namespace. C2J sets {@code metadata.additionalHeaders} to - * {@code {x-amz-glacier-version: }}, which the {@code Request} base class emits - * for ordinary requests. That base stays C2J-generated, so the only gap in the Smithy-generated - * model is the streaming requests ({@code UploadArchive}, {@code UploadMultipartPart}): they derive - * from {@code AmazonStreamingWebServiceRequest} and bypass the base {@code GetHeaders}, so C2J's - * {@code StreamRequestSource.vm} emits the constant header inside their own - * {@code GetRequestSpecificHeaders}. This stamps {@link AdditionalRequestHeadersTrait} on those - * streaming request inputs; request rendering turns it into the matching {@code headers.insert(...)}. - * Self-guards on the raw smithy service name and no-ops when the model has no streaming request. + * Glacier C2J parity. C2J sets metadata.additionalHeaders to {x-amz-glacier-version: }, + * emitted by the C2J-generated base request for ordinary requests. The gap is the streaming requests + * (UploadArchive, UploadMultipartPart), which bypass the base GetHeaders: this stamps + * {@link AdditionalRequestHeadersTrait} on their inputs so request rendering emits the matching + * headers.insert(...). Self-guards on service name; no-op when the model has no streaming request. */ public final class GlacierTransforms { @@ -75,15 +70,11 @@ private static Model addAdditionalHeaders(Model model, ServiceShape service) { return model.toBuilder().addShapes(marked.toArray(new Shape[0])).build(); } - // Upstream Coral2Smithy's GlacierTransformer retypes every header/query `limit` (page-size) member - // from string to smithy.api#Integer, arguing the wire form (a query param) is unchanged. But the - // C++ SDK historically shipped these as Aws::String, so consuming the integer would break the - // public API (Aws::String GetLimit() -> int GetLimit()). This inverts the upstream retype for the - // header/query `limit` members, retargeting them back to the service string shape — matching C2J - // and the sibling string members (e.g. marker). Only these page-size members are query/header - // bound; body `limit` members (whose type change would alter serialization) are never retyped by - // Coral2Smithy and so are already string. Pagination is unaffected: the paginators continue via - // the `Marker` continuation token and never read or write `limit`. + // Upstream Coral2Smithy retypes header/query `limit` members from string to Integer, but the C++ + // SDK historically shipped these as Aws::String, so consuming the integer would break the public + // API. This inverts that retype, retargeting them back to the service string shape to match C2J. + // Body `limit` members are never retyped upstream and stay string; pagination is unaffected (it + // uses the `Marker` continuation token, never `limit`). private static Model retypeLimitQueryMembersToString(Model model, ServiceShape service) { ShapeId stringTarget = ShapeId.fromParts(service.getId().getNamespace(), "string"); if (!model.getShape(stringTarget).isPresent()) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java index a37787ebd3a..1823039213f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java @@ -39,41 +39,36 @@ public final class GlobalTransforms { /** - * Services that skip the "body" -> "requestBody" member rename (raw smithy service names). - * These use "body" as a meaningful domain/payload member. API Gateway (api-gateway) and - * API Gateway V2 (apigatewayv2) are skipped here because their dedicated transforms own the - * rename; the rest use "body" as an HTTP payload. + * Services (raw smithy names) that skip the "body" -> "requestBody" rename. api-gateway/apigatewayv2 + * own the rename in their dedicated transforms; the rest use "body" as a meaningful HTTP payload. */ private static final Set BODY_RENAME_SKIP_SERVICES = Set.of( "amplifyuibuilder", "api-gateway", "apigatewayv2", "bedrock-runtime", "glacier", "repostspace" ); /** - * Services that skip the "headers" -> "headerValues" member rename (raw smithy service name). - * api-gateway renames headers to "requestHeaders" in its dedicated transform instead. + * Services (raw smithy names) that skip the "headers" -> "headerValues" rename. api-gateway + * renames headers to "requestHeaders" in its dedicated transform instead. */ private static final Set HEADERS_RENAME_SKIP_SERVICES = Set.of( "api-gateway" ); /** - * The framework-injected response-envelope member and shape name. It is reserved: no AWS model - * defines its own {@code ResponseMetadata}. {@link #injectResponseMetadata} adds it (and fails - * fast on any pre-existing collision), and {@code MemberRenderer} keys the "always-present" - * rendering (no {@code HasBeenSet} getter, flag initialized true) on this exact name. + * The reserved framework-injected response-envelope member/shape name. {@link #injectResponseMetadata} + * adds it (failing fast on collision), and MemberRenderer keys the "always-present" rendering + * (no HasBeenSet getter, flag initialized true) on this exact name. */ public static final String RESPONSE_METADATA = "ResponseMetadata"; private GlobalTransforms() {} /** - * Renames reserved request members on every operation-input structure: {@code body -> - * requestBody}, {@code headers -> headerValues}, {@code Headers -> headerValues}, honoring the - * per-service skip-lists. Mirrors the legacy C2J {@code RESERVED_REQUEST_MEMBER_MAPPING}. Only - * operation-input shapes are touched (never arbitrary domain shapes that happen to end in - * "Request"). {@link TransformSupport#renameMember} preserves each renamed member's wire name - * via the service's protocol-appropriate trait (matching C2J's {@code setLocationName}), and - * throws on a collision (the target member name already present). + * Renames reserved request members on every operation-input structure ({@code body -> requestBody}, + * {@code headers/Headers -> headerValues}), honoring the per-service skip-lists. Mirrors C2J's + * {@code RESERVED_REQUEST_MEMBER_MAPPING}. Only operation-input shapes are touched. + * {@link TransformSupport#renameMember} preserves each renamed member's wire name via the + * protocol-appropriate trait and throws on a target-name collision. * * @param model the current model * @param service the service being generated (its raw smithy name drives the skip-lists) @@ -128,10 +123,9 @@ private static List> reservedRenames(StructureShape st } /** - * Computes the set of shape IDs reachable from the service's operations; only these shapes - * generate model files. Roots are each operation's input (including {@code smithy.api#Unit} - * for input-less operations), output, and error shapes, from which {@link Walker} walks the - * shape graph transitively. Each root id is included even when its shape is absent from the model. + * Computes the shape IDs reachable from the service's operations; only these generate model files. + * Roots are each operation's input (including {@code smithy.api#Unit}), output, and error shapes, + * walked transitively via {@link Walker}. Each root id is included even if its shape is absent. * * @param model the Smithy model * @param service the service shape whose operations define the root set @@ -154,13 +148,12 @@ private static void addReachableFrom(ShapeId root, Walker walker, Model model, S } /** - * Returns the service's operations excluding any marked {@code @deprecated}. Legacy C2J drops - * deprecated operations entirely (they never appear in the generated client), so their input and - * output structures — orphaned once the operation is gone — are not emitted either. This is the - * single filter used by every emission-driving iteration over the service's operations - * ({@link #computeReachableShapes} and {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier#classify}) - * so reachability and classification stay in agreement. A structure still referenced by a live - * operation remains reachable through that operation, so shared structures are unaffected. + * Returns the service's non-{@code @deprecated} operations. C2J drops deprecated operations + * entirely, so their orphaned input/output structures aren't emitted either. Used by every + * emission-driving iteration ({@link #computeReachableShapes} and + * {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier#classify}) + * so reachability and classification agree; structures still referenced by a live operation + * stay reachable. * * @param model the Smithy model * @param service the service whose operations are being generated @@ -173,11 +166,9 @@ public static List nonDeprecatedOperations(Model model, ServiceS } /** - * Returns this class as a ModelTransform. - * - *

Runs {@link #dropDeprecatedMembers} first (so reachability filtering sees the pruned - * model and orphaned targets drop out), then {@link #renameReservedRequestMembers} to apply the - * C2J-parity request-member renames, then {@link #injectResponseMetadata}. + * Returns this class as a ModelTransform: {@link #dropDeprecatedMembers} first (so reachability + * sees the pruned model), then {@link #renameReservedRequestMembers}, then + * {@link #injectResponseMetadata}. */ public static ModelTransform asTransform() { return (model, service) -> injectResponseMetadata( @@ -185,16 +176,11 @@ public static ModelTransform asTransform() { } /** - * Removes {@code @deprecated} members from the shapes this service actually generates. This - * mirrors the legacy C2J transformer, which drops {@code "deprecated": true} member references - * ({@code C2jModelToGeneratorModelTransformer}), so deprecated members never appear in - * generated model classes. A target shape referenced only through dropped members becomes - * unreachable and is likewise omitted, matching C2J. - * - *

Removal is scoped to members whose container is reachable from the service's operations. - * This is the "only touch what we emit" rule: it leaves framework trait definitions untouched - * (the {@code smithy.api} prelude, {@code smithy.rules}, {@code aws.*}, etc.), some of which - * declare their own {@code @deprecated} members that we must not mutate. + * Removes {@code @deprecated} members from the shapes this service generates, mirroring C2J's + * {@code C2jModelToGeneratorModelTransformer} (which drops {@code "deprecated": true} member refs). + * A target reachable only through dropped members becomes unreachable and is likewise omitted. + * Scoped to members whose container is reachable from the service's operations, so framework + * trait definitions ({@code smithy.api} prelude, {@code smithy.rules}, {@code aws.*}) stay untouched. * * @param model the current model * @param service the service being generated (defines the reachable, emitted shapes) @@ -213,15 +199,11 @@ public static Model dropDeprecatedMembers(Model model, ServiceShape service) { } /** - * For awsQuery / ec2Query services, and for any service carrying the - * {@code aws.protocols#awsQueryCompatible} trait (e.g. SQS = {@code awsJson1_0} + - * {@code @awsQueryCompatible}), injects a {@code ResponseMetadata} structure (carrying - * a {@code RequestId} string member) and adds it as a {@code @required} member on every - * result (operation output) shape. This mirrors the legacy C2J - * {@code CppClientGenerator.addRequestIdToResults} injection (which fires for query/ec2 - * protocols and, via its {@code awsQueryCompatible} branch, for awsQueryCompatible JSON - * services), so that those result classes expose {@code GetResponseMetadata()}. Other - * protocols are unchanged. + * For awsQuery/ec2Query services and any {@code @awsQueryCompatible} service (e.g. SQS = + * {@code awsJson1_0} + {@code @awsQueryCompatible}), injects a {@code ResponseMetadata} structure + * (with a {@code RequestId} member) and adds it as a {@code @required} member on every result shape, + * so those results expose {@code GetResponseMetadata()}. Mirrors C2J's + * {@code CppClientGenerator.addRequestIdToResults}. Other protocols are unchanged. * * @param model the current model * @param service the service being generated @@ -237,16 +219,14 @@ public static Model injectResponseMetadata(Model model, ServiceShape service) { String namespace = service.getId().getNamespace(); ShapeId responseMetadataId = ShapeId.fromParts(namespace, RESPONSE_METADATA); - // ResponseMetadata is reserved. If the model already defines a shape of that name, injecting - // ours would clobber it and MemberRenderer's name-based recognition could not tell them - // apart — fail fast rather than silently mis-generate. + // ResponseMetadata is reserved: a pre-existing shape of that name would be clobbered and + // MemberRenderer's name-based recognition could not tell them apart, so fail fast. if (model.getShape(responseMetadataId).isPresent()) { throw new IllegalStateException("Service " + service.getId() + " already defines a shape '" + responseMetadataId + "'; cannot inject the framework " + RESPONSE_METADATA + " envelope"); } - // ResponseMetadata { RequestId: String } StructureShape responseMetadata = StructureShape.builder() .id(responseMetadataId) .addMember(MemberShape.builder() @@ -255,7 +235,6 @@ public static Model injectResponseMetadata(Model model, ServiceShape service) { .build()) .build(); - // The @required ResponseMetadata member added to each result shape. List replacements = new ArrayList<>(); replacements.add(responseMetadata); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java index d213ad92c68..c89ba423ff8 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTrait.java @@ -9,16 +9,11 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal marker (never declared in any model file) placed by {@link LongPollingTransform} on the - * request structure of each operation for which C2J's {@code RequestHeader.vm} emits - * {@code bool IsLongPollingOperation() const override { return true; }} (gated on - * {@code $operation.longPolling}). C2J sets that flag from - * {@code C2jModelToGeneratorModelTransformer.LONG_POLLING_OPERATIONS}, a hardcoded per-serviceId set - * ({@code SQS: [ReceiveMessage]}, {@code SFN: [GetActivityTask]}, - * {@code SWF: [PollForActivityTask, PollForDecisionTask]}). The transform collapses that lookup into a - * single stamping decision so request rendering only has to turn the marker into the override. Kept as - * a marker + generic renderer rule (not a service-name {@code if}) so the renderer stays - * service-agnostic. + * Internal marker placed by {@link LongPollingTransform} on the request structure of each operation + * C2J flags via {@code LONG_POLLING_OPERATIONS} (SQS ReceiveMessage, SFN GetActivityTask, SWF + * PollForActivityTask/PollForDecisionTask). Request rendering turns the marker into + * {@code IsLongPollingOperation() const override { return true; }} (C2J's {@code RequestHeader.vm} + * gated on {@code $operation.longPolling}). */ public final class LongPollingTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#longPolling"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java index 6ad636277bc..de10d889b75 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java @@ -18,15 +18,11 @@ import java.util.Set; /** - * Stamps the internal {@link LongPollingTrait} onto the request structures of the long-polling - * operations that C2J flags with {@code operation.setLongPolling(true)}, so the - * {@code IsLongPollingOperation() -> true} override is emitted by request rendering. C2J's - * {@code C2jModelToGeneratorModelTransformer.LONG_POLLING_OPERATIONS} keys the set on the C2J - * {@code serviceId} ({@code SQS}, {@code SFN}, {@code SWF}); the Smithy equivalent is the RAW smithy - * service name (the lowercased/hyphenated sdkId from - * {@link ServiceNameUtil#getSmithyServiceName(ServiceShape, Map)} with a {@code null} service map, so - * no c2jMap remap such as {@code sfn->states} is applied). No-op for any other service, leaving the - * model instance untouched. + * Stamps {@link LongPollingTrait} onto the request structures of the long-polling operations C2J flags + * with {@code operation.setLongPolling(true)}, so the {@code IsLongPollingOperation() -> true} override + * is emitted. C2J keys {@code LONG_POLLING_OPERATIONS} on serviceId; the Smithy equivalent is the raw + * smithy service name (from {@link ServiceNameUtil#getSmithyServiceName(ServiceShape, Map)} with a + * {@code null} map, so no c2jMap remap like {@code sfn->states}). No-op for other services. */ public final class LongPollingTransform { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java index dee0eebb12b..518da95e340 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/OverrideStreamingTrait.java @@ -9,14 +9,10 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal marker (never declared in any model file) placed by {@link S3Transforms} on each S3 - * request structure that C2J's {@code S3RestXmlCppClientGenerator} lists in its - * {@code REQUESTS_TO_OVERRIDE_STREAMING} set ({@code shape.setOverrideStreaming(true)}). These - * requests derive from {@code StreamingS3Request} (a typedef for {@code AmazonStreamingWebServiceRequest}, - * whose {@code IsStreaming()} returns {@code true}) yet must report non-streaming, so request rendering - * turns the marker into the {@code bool IsStreaming() const override { return false; }} override that - * {@code RequestHeader.vm} emits under {@code #if($shape.isOverrideStreaming())}. Kept as a marker + - * generic renderer rule (not a service-name {@code if}) so the renderer stays service-agnostic. + * Internal marker placed by {@link S3Transforms} on each S3 request in C2J's + * {@code REQUESTS_TO_OVERRIDE_STREAMING} set. These derive from {@code StreamingS3Request} + * (whose {@code IsStreaming()} returns {@code true}) yet must report non-streaming, so request + * rendering turns the marker into {@code bool IsStreaming() const override { return false; }}. */ public final class OverrideStreamingTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#overrideStreaming"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java index 38c61eba96f..d510bc260f9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java @@ -16,15 +16,11 @@ import java.util.List; /** - * S3 Control model parity with the legacy C2J {@code S3ControlRestXmlCppClientGenerator}, whose - * {@code addRequestIdToResults} adds BOTH a top-level {@code RequestId} and a top-level - * {@code HostId} (x-amz-id-2) to every result. RequestId is already emitted generically - * ({@code ProtocolTraits.resultHasTopLevelRequestId()}); this transform closes the HostId gap by - * marking each operation-output structure with {@link TopLevelHostIdTrait}, which - * {@code ResultRenderer} turns into the top-level HostId accessor group. Self-guards on the raw - * smithy service name {@code s3-control} ({@code ServiceNameUtil.getSmithyServiceName} lowercases - * the {@code S3 Control} sdkId and replaces the space with a hyphen; the {@code s3-control -> - * s3control} c2jMap remap is applied later by the plugin, not here). + * S3 Control parity with C2J's {@code S3ControlRestXmlCppClientGenerator}, whose + * {@code addRequestIdToResults} adds both a top-level {@code RequestId} and {@code HostId} (x-amz-id-2) + * to every result. RequestId is already emitted generically; this closes the HostId gap by marking + * each operation-output with {@link TopLevelHostIdTrait}, which {@code ResultRenderer} turns into the + * top-level HostId accessor group. Self-guards on the raw smithy service name {@code s3-control}. */ public final class S3ControlTransforms { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 4abbb61b85e..1fa5e3dad1e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -36,12 +36,11 @@ import java.util.stream.Collectors; /** - * S3 (and S3-CRT, which shares the S3 model) parity with the legacy C2J - * {@code S3RestXmlCppClientGenerator} for the {@code Model::} namespace. Composes the S3 model - * mutations that C2J applies in {@code generateSourceFiles}. Self-guards on the raw smithy service - * name; every sub-transform no-ops when its target shapes are absent and fast-fails on genuine - * collisions. Client/endpoint/ARN/S3Express/CRT customizations are out of scope (separate - * generators), as is serde-body emission (still stubbed plugin-wide). + * S3 (and S3-CRT, which shares the model) parity with C2J's {@code S3RestXmlCppClientGenerator} for + * the {@code Model::} namespace, composing the model mutations C2J applies in + * {@code generateSourceFiles}. Self-guards on the raw smithy service name; each sub-transform no-ops + * when its shapes are absent and fast-fails on genuine collisions. Client/endpoint/ARN/S3Express/CRT + * customizations and serde-body emission are out of scope. */ public final class S3Transforms { @@ -64,9 +63,8 @@ private static Model apply(Model model, ServiceShape service) { return markChecksumMembers(result, service); } - // C2J's S3RestXmlCppClientGenerator flips these two requests' isOverrideStreaming on. Both derive - // from StreamingS3Request (== AmazonStreamingWebServiceRequest, whose IsStreaming() returns true), - // so they must override IsStreaming() back to false; RequestRenderer emits that for marked shapes. + // C2J flips isOverrideStreaming on for these two requests. Both derive from StreamingS3Request + // (whose IsStreaming() returns true), so they must override it back to false for marked shapes. private static final Set REQUESTS_TO_OVERRIDE_STREAMING = Set.of( "PutBucketPolicyRequest", "PutObjectAnnotationRequest"); @@ -84,10 +82,9 @@ private static Model markOverrideStreaming(Model model) { return model.toBuilder().addShapes(marked.toArray(new Shape[0])).build(); } - // C2J's S3RestXmlCppClientGenerator maps each checksum member shape name to its ChecksumAlgorithm - // enum constant; every request that also carries a ChecksumAlgorithm member gets these members - // flagged so their setters also call SetChecksumAlgorithm(...). ChecksumCRC64NVME is intentionally - // absent (C2J never listed it), so it keeps a plain setter. + // C2J maps each checksum member shape name to its ChecksumAlgorithm enum constant; every request + // that also carries a ChecksumAlgorithm member gets these flagged so their setters also call + // SetChecksumAlgorithm(...). ChecksumCRC64NVME is intentionally absent (C2J never listed it). private static final Map CHECKSUM_MEMBERS_ENUMS = Map.ofEntries( Map.entry("ChecksumCRC32", "CRC32"), Map.entry("ChecksumCRC32C", "CRC32C"), @@ -106,8 +103,8 @@ private static Model markChecksumMembers(Model model, ServiceShape service) { .collect(Collectors.toSet()); List replacements = new ArrayList<>(); for (StructureShape req : model.shapes(StructureShape.class).toList()) { - // Only request shapes that already carry a ChecksumAlgorithm member (so SetChecksumAlgorithm - // exists), and only when they hold at least one not-yet-marked checksum member. + // Only requests already carrying a ChecksumAlgorithm member with at least one + // not-yet-marked checksum member. boolean isChecksumRequest = inputShapes.contains(req.getId()) && req.getMember("ChecksumAlgorithm").isPresent(); boolean needsStamp = isChecksumRequest && req.getAllMembers().values().stream().anyMatch(m -> @@ -115,7 +112,7 @@ private static Model markChecksumMembers(Model model, ServiceShape service) { && !m.hasTrait(ChecksumMemberTrait.class)); if (needsStamp) { // Re-add only the checksum members with the marker; addMember replaces in place, so - // the other members (and the shape's traits/source) carry over untouched via toBuilder. + // other members and the shape's traits/source carry over via toBuilder. StructureShape.Builder b = req.toBuilder(); for (MemberShape m : req.getAllMembers().values()) { String enumValue = CHECKSUM_MEMBERS_ENUMS.get(m.getTarget().getName()); @@ -135,12 +132,10 @@ private static Model markChecksumMembers(Model model, ServiceShape service) { return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } - // C2J's S3RestXmlCppClientGenerator carries a hardcoded functionsWithEmbeddedErrors set; each - // listed request shape gets shape.setEmbeddedErrors(true), which RequestHeader.vm turns into the - // HasEmbeddedError(...) override. Mirror that by stamping EmbeddedErrorsTrait on every request - // structure whose simple name is in the set; REST-XML request rendering emits the method for - // marker-bearing shapes. The lone C2J typo entry (DeleteBucketAnaxlyticsConfigurationRequest) - // is kept verbatim so the set matches C2J exactly; it simply never matches a real shape. + // C2J's hardcoded functionsWithEmbeddedErrors set; each listed request gets setEmbeddedErrors(true), + // rendered as the HasEmbeddedError(...) override. Mirrored by stamping EmbeddedErrorsTrait on every + // request whose simple name is in the set. The C2J typo entry (DeleteBucketAnaxlytics...) is kept + // verbatim to match C2J exactly; it never matches a real shape. private static final Set EMBEDDED_ERROR_REQUESTS = Set.of( "AbortMultipartUploadRequest", "CompleteMultipartUploadRequest", "CopyObjectRequest", "CreateBucketRequest", "CreateMultipartUploadRequest", "CreateSessionRequest", @@ -193,19 +188,18 @@ private static Model markEmbeddedErrors(Model model) { return model.toBuilder().addShapes(marked.toArray(new Shape[0])).build(); } - // C2J's S3RestXmlCppClientGenerator appends a `customizedAccessLogTag` map member - // to every operation request shape, modeled with a distinct `customizedQuery` flag. It binds to - // the query string via @httpQueryParams (so every request emits AddQueryStringParameters), and - // additionally carries the CustomizedAccessLogTagTrait marker so RequestQuerySerializer skips the - // normal map loop for it and instead emits C2J's x--prefix filter block. + // C2J appends a `customizedAccessLogTag` map member to every request. It binds to + // the query string via @httpQueryParams (so every request emits AddQueryStringParameters) and + // carries the CustomizedAccessLogTagTrait marker so RequestQuerySerializer skips the normal map + // loop and emits C2J's x--prefix filter block instead. private static Model injectAccessLogTagQuery(Model model, ServiceShape service) { ShapeId mapId = ShapeId.fromParts("com.amazonaws.s3", "CustomizedAccessLogTag"); ShapeId stringId = ShapeId.from("smithy.api#String"); Set inputShapes = TopDownIndex.of(model).getContainedOperations(service).stream() .map(OperationShape::getInputShape) - // smithy.api#Unit is a shared prelude StructureShape; mutating it would corrupt every - // Unit-input operation across the model, so never treat it as a request shape. + // smithy.api#Unit is a shared prelude shape; mutating it would corrupt every Unit-input + // operation, so never treat it as a request shape. .filter(id -> !id.equals(UnitTypeTrait.UNIT)) .collect(Collectors.toSet()); List updated = model.shapes(StructureShape.class) @@ -225,9 +219,8 @@ private static Model injectAccessLogTagQuery(Model model, ServiceShape service) .addMember(MemberShape.builder() .id(req.getId().withMember("customizedAccessLogTag")) .target(mapId) - // @httpQueryParams binds this map to the query string. C2J models it as a - // querystring member on every request, which is what makes every request emit - // AddQueryStringParameters; the trait drives RequestBindings.hasQueryStringMembers. + // @httpQueryParams binds this map to the query string, driving + // RequestBindings.hasQueryStringMembers so every request emits AddQueryStringParameters. .addTrait(new HttpQueryParamsTrait()) // Marker for C2J's customizedQuery flag: RequestQuerySerializer skips the normal // map loop for this member and emits the x--prefix filter block instead. @@ -238,9 +231,9 @@ private static Model injectAccessLogTagQuery(Model model, ServiceShape service) return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } - // C2J collapses the model's split COMPLETE/COMPLETED ReplicationStatus values into a single - // COMPLETED constant. Drop the extra COMPLETED and rewrite COMPLETE to COMPLETED, preserving - // the remaining member order. Both remove and rewrite means we rebuild the enum explicitly. + // C2J collapses the split COMPLETE/COMPLETED ReplicationStatus values into a single COMPLETED. + // Drop the extra COMPLETED and rewrite COMPLETE to COMPLETED, preserving member order; the + // remove-and-rewrite means the enum is rebuilt explicitly. private static Model normalizeReplicationStatus(Model model) { Optional shapeOpt = model.shapes() .filter(s -> s.getId().getMember().isEmpty()) @@ -288,9 +281,8 @@ private static Map regionNameValueMap() { } // C2J's GetObjectResult carries an x-amz-id-2 header member (Id2) plus the standard RequestId. - // The RequestId is supplied by ResultRenderer's top-level RequestId group for rest-xml results - // (resultHasTopLevelRequestId() == true), which byte-matches C2J; injecting a modeled RequestId - // member here would duplicate it. So inject only Id2. + // RequestId is already supplied by ResultRenderer's top-level group for rest-xml results, so + // injecting a modeled RequestId here would duplicate it; inject only Id2. private static Model hackGetObjectResult(Model model) { String ns = "com.amazonaws.s3"; ShapeId outputId = ShapeId.fromParts(ns, "GetObjectOutput"); @@ -312,10 +304,9 @@ private static Model hackGetObjectResult(Model model) { return model.toBuilder().addShapes(id2Shape, withId2).build(); } - // C2J renames both the CopyObjectResult domain shape (to CopyObjectResultDetails) and the - // CopyObjectOutput member that references it, so the member renders as GetCopyObjectResultDetails - // while keeping its CopyObjectResult wire name. renameMember pins @xmlName("CopyObjectResult") - // for rest-xml so the wire key survives the member-name change. + // C2J renames the CopyObjectResult domain shape (to CopyObjectResultDetails) and the referencing + // CopyObjectOutput member, so it renders as GetCopyObjectResultDetails while keeping its wire name. + // renameMember pins @xmlName("CopyObjectResult") for rest-xml so the wire key survives. private static Model renameCopyObjectResult(Model model, ServiceShape service) { String ns = "com.amazonaws.s3"; ShapeId oldId = ShapeId.fromParts(ns, "CopyObjectResult"); @@ -391,12 +382,10 @@ private static Model addExpiresCustomization(Model model, ServiceShape service) return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); } - // Both request and result on ListParts / GetObjectAttributes reference these two shapes. C2J models - // them as integers, so the shipped SDK exposes int accessors. Coral2Smithy's S3ShapeMutatorTransformer - // instead treats them as opaque pagination tokens: it leaves PartNumberMarker as Coral's string and - // retypes NextPartNumberMarker to string. Retype both back to integer here to preserve the C2J public - // API (int, not Aws::String). The paginator generator is a separate plugin that never sees this - // mutation; it keeps its own NUMERIC_TOKEN_OVERRIDES entry so its `!= 0` check matches the int result. + // C2J models these two shapes (used by ListParts / GetObjectAttributes) as integers, so the shipped + // SDK exposes int accessors; Coral2Smithy retypes them to string pagination tokens. Retype both back + // to integer to preserve the C2J public API. The paginator generator is a separate plugin that never + // sees this and keeps its own NUMERIC_TOKEN_OVERRIDES entry so its `!= 0` check matches the int result. private static final List PART_NUMBER_MARKER_SHAPES = List.of("PartNumberMarker", "NextPartNumberMarker"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java index c844dc49107..b8ec549a219 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java @@ -20,11 +20,9 @@ import java.util.Set; /** - * Injects a synthetic {@code SourceRegion} string member into the request shapes of the - * cross-region copy operations for RDS-family services. Mirrors the legacy C2J - * {@code RDSCppClientGenerator}/{@code DocDBCppClientGenerator}/{@code NeptuneCppClientGenerator} - * injection that backs presigned-URL generation. Model-shape scope only: the presigned-URL - * client logic remains in the C2J path, which references this member. + * Injects a synthetic {@code SourceRegion} string member into the cross-region copy request shapes of + * RDS-family services (RDS/DocDB/Neptune), mirroring the C2J injection that backs presigned-URL + * generation. Model-shape scope only; the presigned-URL client logic remains in the C2J path. */ public final class SourceRegionTransform { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java index 8dd4b6eaa04..76e2ba8a735 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTrait.java @@ -9,14 +9,10 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal marker (never declared in any model file) placed by {@link SupportsPresigningTransform} - * on each request structure that C2J's generators flag with {@code shape.setSupportsPresigning(true)} - * (every query/ec2 request via {@code QueryCppClientGenerator}, plus Polly's {@code SynthesizeSpeech}). - * C2J's shared {@code RequestHeader.vm} emits the protected {@code DumpBodyToUrl(Aws::Http::URI&)} - * override under {@code #if($shape.supportsPresigning())}, independent of protocol; request rendering - * turns this marker into that same protected override so the declaration stays protocol-agnostic - * while each protocol supplies only the method body. Kept as a marker + generic renderer rule (not a - * service-name {@code if}) so the renderer stays service-agnostic. + * Internal marker placed by {@link SupportsPresigningTransform} on each request C2J flags with + * {@code setSupportsPresigning(true)} (every query/ec2 request, plus Polly's {@code SynthesizeSpeech}). + * Request rendering turns the marker into the protected, protocol-agnostic + * {@code DumpBodyToUrl(Aws::Http::URI&)} override, with each protocol supplying only the method body. */ public final class SupportsPresigningTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#supportsPresigning"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java index d61c53db417..3eb51cfce34 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java @@ -18,14 +18,11 @@ import java.util.List; /** - * Stamps the internal {@link SupportsPresigningTrait} onto the OPERATIONS that C2J flags with - * {@code shape.setSupportsPresigning(true)}, so the protocol-agnostic {@code DumpBodyToUrl} override - * (declaration and impl) is emitted by request rendering. In C2J {@code supportsPresigning} is - * conceptually per-operation; the trait is stamped on the operation (never shared, unlike the - * {@code smithy.api#Unit} input) so it also covers {@code Unit}-input operations and keeps the decl - * and impl symmetric and protocol-agnostic. C2J's {@code QueryCppClientGenerator} sets the flag for - * every query/ec2 operation; Polly additionally sets it on {@code SynthesizeSpeech}. No-op for any - * other service, leaving the model instance untouched. + * Stamps {@link SupportsPresigningTrait} onto the operations C2J flags with + * {@code setSupportsPresigning(true)}, so the protocol-agnostic {@code DumpBodyToUrl} override is + * emitted. Stamped on the operation (never shared, unlike the {@code smithy.api#Unit} input) so it + * also covers Unit-input operations. C2J sets the flag for every query/ec2 operation, plus Polly's + * {@code SynthesizeSpeech}. No-op for other services. */ public final class SupportsPresigningTransform { @@ -44,8 +41,7 @@ private static Model apply(Model model, ServiceShape service) { } List updated = new ArrayList<>(); for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { - // Operations are never Unit, so query/ec2 stamps every operation (covering Unit-input - // ops). Idempotent: skip operations that already carry the trait. + // Operations are never Unit, so query/ec2 stamps every operation. Idempotent. boolean target = queryLike || "SynthesizeSpeech".equals(op.getId().getName()); if (target && !op.hasTrait(SupportsPresigningTrait.class)) { updated.add(op.toBuilder().addTrait(new SupportsPresigningTrait()).build()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java index 70f0eab371c..0ff68d7beb3 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TopLevelHostIdTrait.java @@ -9,12 +9,10 @@ import software.amazon.smithy.model.traits.AnnotationTrait; /** - * Internal marker (never declared in any model file) placed by {@link S3ControlTransforms} on each - * S3 Control operation-output structure. {@code ResultRenderer} emits the top-level {@code HostId} - * (x-amz-id-2) accessor group for marker-bearing result shapes — mirroring how the sibling - * top-level {@code RequestId} is emitted — so S3 Control results match C2J - * ({@code addToAllResultsShape("hostId", ...)}). Kept as a marker + generic renderer rule (not a - * service-name {@code if}) so the renderer stays service-agnostic. + * Internal marker placed by {@link S3ControlTransforms} on each S3 Control operation-output. For + * marker-bearing results, {@code ResultRenderer} emits the top-level {@code HostId} (x-amz-id-2) + * accessor group (mirroring the sibling top-level {@code RequestId}), matching C2J's + * {@code addToAllResultsShape("hostId", ...)}. */ public final class TopLevelHostIdTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("aws.cpp.internal#topLevelHostId"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java index 3d6df1ca2cf..5a60a828ca5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/TransformSupport.java @@ -39,19 +39,12 @@ final class TransformSupport { private TransformSupport() {} /** - * Appends the given wire {@code values} to an enum shape. + * Appends the given wire {@code values} to an enum shape. Each value must be identifier-safe + * ({@code [A-Za-z_][A-Za-z0-9_]*}) since it doubles as the Smithy member name and is compared + * against existing values for dedup; non-identifier values are rejected up front. * - *

Precondition: each value MUST be an identifier-safe wire value, i.e. a - * valid Smithy enum member name matching {@code [A-Za-z_][A-Za-z0-9_]*}. Values containing - * characters such as {@code '-'}, {@code '.'}, or spaces are rejected. This precondition matters - * for two reasons: the idempotency dedup compares the incoming values against the shape's - * existing values (obtained via {@link EnumRenderer#getEnumValues(Shape)}), and the - * {@code EnumShape} branch uses each value directly as the Smithy member name via - * {@code builder.addMember(value, value)}. A non-identifier value would silently break dedup - * and fail deep inside Smithy, so it is rejected up front. - * - * @param enumShape the enum shape (Smithy 2.0 {@code EnumShape} or legacy {@code StringShape} - * with an {@code @enum} trait) to append to + * @param enumShape the enum shape (Smithy 2.0 {@code EnumShape} or legacy {@code @enum} + * {@code StringShape}) to append to * @param values identifier-safe wire values to append * @return the updated shape, or {@link Optional#empty()} if all values are already present * @throws IllegalArgumentException if any value is not an identifier-safe enum member name @@ -90,19 +83,11 @@ static Optional appendValues(Shape enumShape, List values) { /** * Appends {@code name -> value} enum entries, allowing wire values that are not - * identifier-safe (e.g. region strings containing {@code '-'} such as {@code us-east-1}). This is - * the name/value counterpart of {@link #appendValues(Shape, List)}, which requires the wire value - * to double as the member name. - * - *

Each map key is the Smithy member name and MUST be identifier-safe (matching - * {@code [A-Za-z_][A-Za-z0-9_]*}); each map value is the wire value and may be arbitrary. For a - * Smithy 2.0 {@code EnumShape} the name becomes the member name and the value the - * {@code @enumValue} via {@code builder.addMember(name, value)}; for a legacy {@code @enum} - * {@code StringShape} only the wire value is recorded (matching C2J, which keys the enum off the - * wire value and derives the constant name by sanitizing it). - * - *

Idempotent: entries whose wire value already exists are skipped; if every value is already - * present the shape is returned unchanged as {@link Optional#empty()}. + * identifier-safe (e.g. region strings like {@code us-east-1}); the name/value counterpart of + * {@link #appendValues(Shape, List)}. Each map key is the member name and must be identifier-safe; + * the value is the arbitrary wire value. For a Smithy 2.0 {@code EnumShape} both are recorded via + * {@code addMember(name, value)}; for a legacy {@code @enum} {@code StringShape} only the wire + * value is recorded (matching C2J). Idempotent: entries whose wire value already exists are skipped. * * @param enumShape the enum shape to append to * @param nameToValue ordered member-name to wire-value entries to append @@ -144,14 +129,10 @@ static Optional appendEnumValues(Shape enumShape, Map nam } /** - * Locates an enum shape by its simple (relative) name and appends the given identifier-safe wire - * {@code values}, returning the model with the updated shape — or unchanged when the shape is - * absent or every value is already present. Wraps the per-service pattern of adding unmodeled - * enum values; see {@link #appendValues} for the identifier-safe precondition on {@code values}. - * - *

The lookup matches the first shape whose relative name equals {@code simpleName} and which - * is an enum (Smithy 2.0 {@code EnumShape} or a legacy {@code StringShape} with an {@code @enum} - * trait). Callers are expected to have already scoped generation to a single service. + * Locates an enum shape by simple (relative) name and appends the identifier-safe wire + * {@code values}, returning the model with the updated shape — or unchanged if the shape is absent + * or every value is already present. See {@link #appendValues} for the value precondition. Matches + * the first enum shape whose relative name equals {@code simpleName}; callers scope to one service. */ static Model appendEnumValuesByName(Model model, String simpleName, List values) { return findEnumByName(model, simpleName) @@ -161,11 +142,9 @@ static Model appendEnumValuesByName(Model model, String simpleName, List } /** - * Locates an enum shape by its simple (relative) name and appends the given {@code member-name -> - * wire-value} entries (allowing non-identifier-safe wire values, e.g. region strings), returning - * the model with the updated shape — or unchanged when the shape is absent or every value is - * already present. Wraps the per-service pattern; see {@link #appendEnumValues} for the - * member-name precondition and value semantics. + * Locates an enum shape by simple (relative) name and appends the {@code member-name -> wire-value} + * entries (allowing non-identifier-safe wire values), returning the model with the updated shape — + * or unchanged if absent or all present. See {@link #appendEnumValues} for the semantics. */ static Model appendEnumEntriesByName(Model model, String simpleName, Map nameToValue) { return findEnumByName(model, simpleName) @@ -194,20 +173,13 @@ private static List existingWireValues(Shape enumShape) { /** * Returns a copy of {@code struct} with member {@code oldName} renamed to {@code newName}, - * preserving member declaration order and copying all traits onto the renamed member. Returns - * {@link Optional#empty()} if {@code oldName} is absent (nothing to rename). - * - *

The renamed member keeps its original wire name. A member with no explicit wire-name trait - * serializes under its member name, so renaming it would silently change the wire key; to - * prevent that this method pins the original name via the protocol-appropriate trait(s) - * ({@link #wireNamePreservingTraits}). This mirrors the legacy C2J rename primitive, which - * couples {@code setLocationName(originalMemberKey)} into the same step that changes the member - * key so a rename can never drop the wire name. + * preserving declaration order and copying all traits; {@link Optional#empty()} if {@code oldName} + * is absent. The renamed member keeps its original wire name: since a member with no wire-name + * trait serializes under its member name, this pins the original name via the protocol-appropriate + * trait(s) ({@link #wireNamePreservingTraits}), mirroring C2J's rename+{@code setLocationName}. * - * @throws IllegalStateException if {@code newName} is already a distinct member — a genuine - * collision that would silently drop a member; or if the protocol has no wire-name trait - * to preserve the original key (see {@link #wireNamePreservingTrait}). Callers must not - * mask either. + * @throws IllegalStateException if {@code newName} is already a distinct member (a collision that + * would drop a member), or if the protocol has no wire-name trait to preserve the key. */ static Optional renameMember(StructureShape struct, String oldName, String newName, Protocol protocol) { @@ -236,26 +208,20 @@ static Optional renameMember(StructureShape struct, String oldNa } /** - * The trait(s) to add to the renamed member so its wire name(s) stay equal to what {@code oldName} - * produced. Existing wire-name traits are always copied verbatim by the rename, so this only - * synthesizes what the member lacks; if a protocol's trait is already present, nothing is added - * for it. + * The trait(s) to add so the renamed member's wire name(s) stay equal to what {@code oldName} + * produced. Existing wire-name traits are copied verbatim by the rename, so this only synthesizes + * what the member lacks. * *

    *
  • JSON-family ({@code awsJson}, {@code restJson1}): {@code @jsonName}.
  • *
  • {@code restXml} / {@code awsQuery}: {@code @xmlName}.
  • - *
  • {@code ec2Query}: request and response use different names, so both are pinned. - * The request query key is authoritative from {@code @ec2QueryName} (used verbatim); the - * response XML element is {@code @xmlName}. EC2 models routinely carry an {@code @xmlName} - * that is not merely the camelCase of the request key (e.g. member {@code Ipv6Addresses} - * has {@code ec2QueryName=Ipv6Addresses} but {@code xmlName=ipv6AddressesSet}), so - * reconstructing the request key from {@code capitalize(@xmlName)} — what legacy C2J does — - * is unreliable. We instead pin {@code @ec2QueryName} to the member's current request key - * ({@code capitalize(@xmlName ?? memberName)} when it has none of its own) so it survives - * the member-name change without depending on any serde-time fallback.
  • - *
  • Any other protocol (e.g. {@code rpcv2Cbor}, which has no wire-name trait and always - * serializes under the member name): fail fast rather than emit an inert trait and - * mis-generate later.
  • + *
  • {@code ec2Query}: request and response use different names, so both are pinned — + * {@code @ec2QueryName} for the request key, {@code @xmlName} for the response element. + * EC2's {@code @xmlName} is often not the camelCase of the request key (e.g. + * {@code Ipv6Addresses} vs {@code ipv6AddressesSet}), so rather than reconstruct it we pin + * {@code @ec2QueryName} to the current request key ({@code capitalize(@xmlName ?? memberName)}).
  • + *
  • Any other protocol (e.g. {@code rpcv2Cbor}, which has no wire-name trait): fail fast rather + * than emit an inert trait and mis-generate.
  • *
*/ private static List wireNamePreservingTraits(MemberShape member, String oldName, diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java index d29a3b6a107..9232e21bf08 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/CppTypeMapperTest.java @@ -254,9 +254,8 @@ void enumShape_lowerCamelName_mapsToUpperCamelType() { @Test void legacyEnumStringShape_mapsToEnumType() { - // Smithy 1.0 models a closed set as a `string` shape carrying the @enum trait (not a 2.0 - // EnumShape). C2J treats these as enums, so a member targeting one must resolve to the - // enum C++ type, NOT Aws::String. Mirrors ShapeClassifier / EnumResolver detection. + // Smithy 1.0 models a closed set as a `string` shape with the @enum trait. C2J treats these + // as enums, so a member targeting one resolves to the enum C++ type, not Aws::String. software.amazon.smithy.model.traits.EnumTrait enumTrait = software.amazon.smithy.model.traits.EnumTrait.builder() .addEnum(software.amazon.smithy.model.traits.EnumDefinition.builder() @@ -317,11 +316,7 @@ void mapOfStringToStruct_mapsToAwsMap() { // --- recursive (mutually-referenced) shape tests --- - /** - * Builds the connectcases-style mutual cycle: a union {@code BooleanCondition} with a direct - * member targeting struct {@code CompoundCondition}, which holds a list of - * {@code BooleanCondition}. The two aggregates are mutually referenced through the list. - */ + /** connectcases-style mutual cycle: union {@code BooleanCondition} -> struct {@code CompoundCondition} -> list of {@code BooleanCondition}. */ private static Model mutualCycleModel() { StructureShape operands = StructureShape.builder().id("com.example#BooleanOperands").build(); // Forward references are fine; Model.builder resolves them at build(). @@ -378,9 +373,8 @@ void recursiveMember_headerSwapsStructIncludeForAllocator_andForwardDeclares() { @Test void directSelfReference_isRecursive_butNotForwardDeclaredOrSelfIncluded() { - // connectcases CaseFilter has a `not` member targeting CaseFilter itself. C2J renders it as - // std::shared_ptr but adds neither a self forward-declaration nor a self-include - // (the class declares itself) — and, unlike the mutual case, no AWSAllocator.h either. + // connectcases CaseFilter's `not` member targets CaseFilter itself: C2J renders shared_ptr + // but adds no self forward-decl, no self-include, and (unlike the mutual case) no AWSAllocator.h. StructureShape filter = StructureShape.builder() .id("com.example#CaseFilter") .addMember("not", software.amazon.smithy.model.shapes.ShapeId.from("com.example#CaseFilter")) @@ -589,9 +583,8 @@ void getIncludesForShape_withListMember_includesVectorAndElement() { @Test void getIncludesForShape_withIdempotencyTokenMember_includesUuidHeader() { - // C2J adds to any shape carrying an @idempotencyToken member, - // because such members are brace-initialized with Aws::Utils::UUID::PseudoRandomUUID() - // (CppViewHelper.java). The include must be present for the initializer to compile. + // C2J adds for any @idempotencyToken member, which is brace-initialized + // with Aws::Utils::UUID::PseudoRandomUUID() and needs the include to compile. StringShape str = StringShape.builder().id("com.example#Str").build(); StructureShape struct = StructureShape.builder() .id("com.example#MyRequest") @@ -644,10 +637,8 @@ void getIncludesForShape_withMapMember_includesMapKeyAndValue() { @Test void getIncludesForShape_withNestedMapOfMap_includesLeafStructHeader() { - // apigateway Deployment.apiSummary is Map>. The outer - // map's value is itself a map (no header of its own), so a one-level unwrap stops before - // reaching the leaf struct MethodSnapshot and its header is dropped — an incomplete-type - // compile error. C2J recursively unwraps nested containers to include all leaf headers. + // apigateway Deployment.apiSummary is Map>: a one-level + // unwrap misses the leaf MethodSnapshot header, so C2J recursively unwraps nested containers. StringShape str = StringShape.builder().id("com.example#Str").build(); StructureShape leaf = StructureShape.builder().id("com.example#MethodSnapshot").build(); MapShape innerMap = MapShape.builder() diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java index f22a5581945..8dae2e12f55 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EnumRendererTest.java @@ -43,9 +43,8 @@ void renderHeader_producesEnumClass() { @Test void renderHeader_capitalizesLowerCamelEnumName() { - // Some Smithy models (e.g. IAM) name enum shapes in lowerCamel. C2J normalizes every - // shape name to UpperCamel, so the C++ enum type, mapper namespace, and mapper - // functions must be capitalized regardless of the model's casing. + // Some Smithy models (e.g. IAM) name enum shapes in lowerCamel; C2J normalizes to UpperCamel, + // so the C++ enum type, mapper namespace, and functions must all be capitalized. EnumShape enumShape = EnumShape.builder() .id("com.example#statusType") .addMember("Active", "Active") diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java index 0b4bf2a5e11..6545cc4e58f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventPayloadRendererTest.java @@ -24,9 +24,7 @@ /** * Verifies {@link EventPayloadRenderer} produces a header-only blob-carrier event (C2J - * {@code EventHeader.vm} form): an {@code Aws::Vector} payload with a bytes - * constructor, non-template accessors, and a {@code GetWithOwnership()} move-out — and - * NO JSON serde and NO {@code .cpp}. + * {@code EventHeader.vm} form): {@code Aws::Vector} payload, no JSON serde, no {@code .cpp}. */ class EventPayloadRendererTest { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java index d04d8a98092..82925e48bd7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java @@ -87,9 +87,8 @@ private static Model twoEventModel() { return Model.builder().addShapes(str, stream, eventA, eventB, exc, modeledExc, input, output, op, service).build(); } - // A @streaming union with one empty event (target shape has no modeled members) and one data - // event (target shape has a modeled member). Callback/member names derive from the target - // shape name, matching twoEventModel's convention (alpha -> AlphaEvent -> m_onAlphaEvent). + // A @streaming union with one empty event (no modeled members) and one data event. Callback/member + // names derive from the target shape name (alpha -> AlphaEvent -> m_onAlphaEvent). private static Model unionWithEmptyAndDataEvent() { StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape emptyEvent = StructureShape.builder() @@ -209,8 +208,7 @@ void handlerHeader_wrapsInitialResponseSettersInDocGroup() { @Test void eventStreamUnionHeader_noLongerEmitted() { // The incoming event-stream union is realized via the handler; nothing references it as a - // data type. The renderer must not emit its standalone .h (dead public API) — the - // classifier already drops it from subObjects so no other renderer emits it either. + // data type, so the renderer must not emit its standalone .h (dead public API). java.util.List paths = renderedFilePaths(twoEventModel()); assertTrue(paths.stream().noneMatch(p -> p.endsWith("MyStreamEventStream.h")), "incoming event-stream union header must not be emitted: " + paths); @@ -240,9 +238,8 @@ void initialResponseHeader_hasHeaderCollectionCtorAndSerdeDecls() { @Test void initialResponseHeader_rendersNonStreamingResultMembers() { - // C2J synthesizes InitialResponse from the result's non-event-stream members, so the - // header must carry accessors for those members (here: contentType) plus a private section. - // The @httpPayload streaming union member (stream) must NOT appear. + // C2J synthesizes InitialResponse from the result's non-event-stream members (here + // contentType); the @httpPayload streaming union member (stream) must not appear. String h = render("DoStreamInitialResponse.h"); assertTrue(h.contains("GetContentType") && h.contains("SetContentType") && h.contains("WithContentType"), "InitialResponse must render accessors for non-streaming result members: " + h); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index 1198a76430d..34c2568221d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java @@ -171,7 +171,6 @@ private static Model inputModelWithProtocol(String sdkId, @Test void computeReachableShapes_simpleOperation_includesInputAndOutput() { - // Build a minimal model with one operation that has input and output StructureShape input = StructureShape.builder() .id("com.example#MyInput") .addMember(MemberShape.builder() @@ -212,7 +211,6 @@ void computeReachableShapes_simpleOperation_includesInputAndOutput() { @Test void computeReachableShapes_nestedStructure_isReachable() { - // Nested structure should be reachable through member reference StructureShape nested = StructureShape.builder() .id("com.example#NestedStruct") .addMember(MemberShape.builder() @@ -257,7 +255,6 @@ void computeReachableShapes_nestedStructure_isReachable() { @Test void computeReachableShapes_unreferencedShape_notIncluded() { - // A shape that is not referenced by any operation should not be reachable StructureShape unreferenced = StructureShape.builder() .id("com.example#Unreferenced") .addMember(MemberShape.builder() @@ -297,7 +294,6 @@ void computeReachableShapes_unreferencedShape_notIncluded() { @Test void computeReachableShapes_listMember_targetIsReachable() { - // List member targets should be traversed StructureShape element = StructureShape.builder() .id("com.example#Element") .addMember(MemberShape.builder() @@ -350,7 +346,6 @@ void computeReachableShapes_listMember_targetIsReachable() { @Test void computeReachableShapes_mapKeyAndValue_areReachable() { - // Map key and value targets should be traversed StructureShape valueShape = StructureShape.builder() .id("com.example#MapValue") .addMember(MemberShape.builder() @@ -407,7 +402,6 @@ void computeReachableShapes_mapKeyAndValue_areReachable() { @Test void computeReachableShapes_errorShapes_areReachable() { - // Error shapes should be traversed StructureShape error = StructureShape.builder() .id("com.example#MyError") .addMember(MemberShape.builder() @@ -449,9 +443,8 @@ void computeReachableShapes_errorShapes_areReachable() { @Test void computeReachableShapes_excludesStructReachableOnlyViaDeprecatedOperation() { - // A @deprecated operation is dropped entirely (matching legacy C2J, which omits deprecated - // operations). A struct reachable ONLY through the deprecated op's input must fall out of the - // reachable set, while a struct shared with a live op stays reachable via that live op. + // A @deprecated operation is dropped entirely (C2J parity). A struct reachable only via it + // falls out of the reachable set; one shared with a live op stays reachable. StructureShape deprecatedOnly = StructureShape.builder() .id("com.example#DeprecatedOnly") .addMember(MemberShape.builder() @@ -512,9 +505,8 @@ void computeReachableShapes_excludesStructReachableOnlyViaDeprecatedOperation() @Test void dropDeprecatedMembers_removesDeprecatedMember_keepsOthers() { - // Mirrors C2J: a member with @deprecated is dropped from the generated shape; siblings stay. - // The shape must be reachable from the service (used as an operation input here) so that the - // reachability-scoped transform considers it. + // Mirrors C2J: a @deprecated member is dropped; siblings stay. The shape is an operation + // input so the reachability-scoped transform considers it. StructureShape config = StructureShape.builder() .id("com.example#LocationConfiguration") .addMember(MemberShape.builder() @@ -590,9 +582,8 @@ void dropDeprecatedMembers_orphanedTargetBecomesUnreachable() { @Test void dropDeprecatedMembers_sharedTargetSurvivesViaNonDeprecatedReference() { - // A shape reached through BOTH a @deprecated member and a live member must stay reachable: - // dropping the deprecated reference must never orphan a shape the surviving model still uses. - // This guards against a pruning bug that would drop a shared shape and dangle the live ref. + // A shape reached through both a @deprecated and a live member must stay reachable: dropping + // the deprecated reference must not orphan a shape the surviving model still uses. StructureShape shared = StructureShape.builder() .id("com.example#SharedDetail") .addMember(MemberShape.builder() @@ -636,9 +627,8 @@ private static ServiceShape serviceOf(Model model, String name) { @Test void injectResponseMetadata_failsFastOnModeledResponseMetadataMember() { - // ResponseMetadata is framework-reserved. A modeled member of that name on a result would - // make MemberRenderer's name-based recognition ambiguous, so injection fails fast rather - // than clobber the modeled member or silently mis-render it. + // ResponseMetadata is framework-reserved; a modeled member of that name would make + // name-based recognition ambiguous, so injection fails fast rather than clobber it. StructureShape input = StructureShape.builder().id("com.example#DoThingInput").build(); StructureShape output = StructureShape.builder() .id("com.example#DoThingOutput") @@ -791,9 +781,8 @@ void injectResponseMetadata_awsJsonWithQueryCompatible_addsResponseMetadataStruc @Test void injectResponseMetadata_skipsDeprecatedOperationOutputSharedByLiveOp() { - // A @deprecated operation's output that is ALSO reused as a nested member by a live op's - // output is reachable/emitted as a sub-object, but C2J drops the deprecated op entirely and - // never injects ResponseMetadata into that shape. Only genuine live-op outputs get it. + // A @deprecated op's output reused as a nested member stays emitted, but C2J drops the + // deprecated op and never injects ResponseMetadata there; only genuine live-op outputs get it. StructureShape sharedOutput = StructureShape.builder() .id("com.example#SharedOutput") .addMember(MemberShape.builder() diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java index 7925f00304e..cfc961f1168 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java @@ -40,7 +40,6 @@ void fullOutput_matchesExpectedPattern() { System.out.println("=== PUBLIC ==="); System.out.println(pubOutput); - // Verify ShardId accessor pattern assertTrue(pubOutput.contains("inline const Aws::String& GetShardId() const { return m_shardId; }")); assertTrue(pubOutput.contains("inline bool ShardIdHasBeenSet() const { return m_shardIdHasBeenSet; }")); assertTrue(pubOutput.contains("template ")); @@ -50,16 +49,13 @@ void fullOutput_matchesExpectedPattern() { assertTrue(pubOutput.contains("ChildShard& WithShardId(ShardIdT&& value)")); assertTrue(pubOutput.contains("SetShardId(std::forward(value));")); - // Verify ParentShards list pattern - includes Add method assertTrue(pubOutput.contains("inline const Aws::Vector& GetParentShards() const { return m_parentShards; }")); assertTrue(pubOutput.contains("ChildShard& AddParentShards(ParentShardsT&& value)")); assertTrue(pubOutput.contains("m_parentShards.emplace_back(std::forward(value));")); - // Verify no Add method for non-list members assertFalse(pubOutput.contains("AddShardId")); assertFalse(pubOutput.contains("AddHashKeyRange")); - // Private section CppWriter privWriter = new CppWriter(); MemberRenderer.forStructure(model, shape, null).renderPrivateSection(privWriter); String privOutput = privWriter.toString(); @@ -115,9 +111,8 @@ void nonChecksumStringMember_hasNoAlgorithmSideEffectOrConstCharOverload() { @Test void sparseListAndMap_emitOptionalTypesAndAddOverloads() { - // Mirrors C2J's generated SparseNullsOperationRequest.h: a @sparse list/map wraps its - // element/value in Aws::Crt::Optional, and gets an extra Add overload accepting the - // Optional element/value directly. + // Mirrors C2J SparseNullsOperationRequest.h: @sparse list/map wraps element/value in + // Aws::Crt::Optional and gets an extra Add overload taking the Optional directly. StringShape str = StringShape.builder().id("com.example#String").build(); ListShape sparseList = ListShape.builder() .id("com.example#SparseStringList") @@ -142,7 +137,6 @@ void sparseListAndMap_emitOptionalTypesAndAddOverloads() { String out = writer.toString(); System.out.println(out); - // --- sparse list --- assertTrue(out.contains( "inline const Aws::Vector>& GetSparseStringList() const { return m_sparseStringList; }"), out); @@ -155,7 +149,6 @@ void sparseListAndMap_emitOptionalTypesAndAddOverloads() { out); assertTrue(out.contains("m_sparseStringList.push_back(value);"), out); - // --- sparse map --- assertTrue(out.contains( "inline const Aws::Map>& GetSparseStringMap() const { return m_sparseStringMap; }"), out); @@ -172,9 +165,8 @@ void sparseListAndMap_emitOptionalTypesAndAddOverloads() { @Test void recursiveMember_rendersSharedPtrFieldGetterAndMakeSharedSetter() { - // Mirrors connectcases BooleanCondition.h: the andAll member targets CompoundCondition, - // which lists BooleanCondition back — a cycle C2J breaks with std::shared_ptr, a *m_x - // getter, and a MakeShared setter tagged with the enclosing class name. + // Mirrors connectcases BooleanCondition.h: the andAll->CompoundCondition->BooleanCondition + // cycle C2J breaks with std::shared_ptr, a *m_x getter, and an enclosing-class MakeShared setter. StructureShape operands = StructureShape.builder().id("com.example#BooleanOperands").build(); UnionShape booleanCondition = UnionShape.builder() .id("com.example#BooleanCondition") diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java index 598095009a8..354199013f5 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererTest.java @@ -38,9 +38,8 @@ void renderPublicSection_stringMember_producesGetSetWith() { @Test void renderPublicSection_lowercaseMember_capitalizesMethodNames() { - // Smithy member names frequently start lowercase (e.g. "extendedKeyUsage"). - // The legacy C2J convention capitalizes the accessor method names and template - // params while keeping the decapitalized field name (m_extendedKeyUsage). + // Smithy member names often start lowercase; C2J capitalizes accessor/template names while + // keeping the decapitalized field name (m_extendedKeyUsage). StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape shape = StructureShape.builder() .id("com.example#MyShape") @@ -55,7 +54,6 @@ void renderPublicSection_lowercaseMember_capitalizesMethodNames() { assertTrue(output.contains("SetExtendedKeyUsage(ExtendedKeyUsageT&& value)"), "Setter should be capitalized: " + output); assertTrue(output.contains("WithExtendedKeyUsage(ExtendedKeyUsageT&& value)"), "With should be capitalized: " + output); assertTrue(output.contains("m_extendedKeyUsage = std::forward"), "Field should stay decapitalized: " + output); - // Must not emit the raw lowercase-first method names. assertFalse(output.contains("GetextendedKeyUsage"), "Must not emit lowercase getter: " + output); assertFalse(output.contains("WithextendedKeyUsage"), "Must not emit lowercase With: " + output); } @@ -177,9 +175,8 @@ void renderPublicSection_mapWithNonPrimitiveKeyAndValue_addIsTemplated() { @Test void renderPublicSection_documentMember_getterReturnsDocumentViewByValue() { - // C2J special-cases the document getter to return Aws::Utils::DocumentView by value - // (ModelClassMembersAndInlines.vm: $returnType = "Aws::Utils::DocumentView"), while the - // field and setter stay Aws::Utils::Document. + // C2J special-cases the document getter to return Aws::Utils::DocumentView by value, while + // the field and setter stay Aws::Utils::Document. software.amazon.smithy.model.shapes.DocumentShape doc = software.amazon.smithy.model.shapes.DocumentShape.builder().id("com.example#Doc").build(); StructureShape shape = StructureShape.builder() @@ -251,7 +248,6 @@ void renderPublicSection_primitiveGetter_returnsByValue() { CppWriter writer = new CppWriter(); MemberRenderer.forStructure(model, shape, "MyShape").renderPublicAccessors(writer); String output = writer.toString(); - // Primitive getter should return by value, not const ref assertTrue(output.contains("inline int GetCount() const"), "Primitive should return by value: " + output); assertFalse(output.contains("inline const int&"), "Should NOT return const ref for primitives: " + output); } @@ -308,10 +304,8 @@ void renderPrivateSection_nonPrimitiveMembers_noDefaultValue() { @Test void renderPrivateSection_idempotencyTokenMember_initializesWithPseudoRandomUuidAndHasBeenSetTrue() { - // C2J auto-populates @idempotencyToken members with a random UUID at construction - // (ServiceClientModelHeaderMemberDeclaration.vm) and flags them as already-set - // (ModelClassMembersAndInlines.vm), so a caller who omits the token still gets - // idempotent behavior. The initializer and the =true flag must both be emitted. + // C2J auto-populates @idempotencyToken members with a random UUID and flags them already-set, + // so a caller who omits the token still gets idempotent behavior. StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape shape = StructureShape.builder() .id("com.example#MyShape") @@ -369,7 +363,6 @@ void renderPrivateSection_hasBeenSetFlagsGroupedAtEnd() { CppWriter writer = new CppWriter(); MemberRenderer.forStructure(model, shape, null).renderPrivateSection(writer); String output = writer.toString(); - // HasBeenSet flags should come after data members int nameFieldPos = output.indexOf("Aws::String m_name;"); int countFieldPos = output.indexOf("int m_count{0};"); int nameHasBeenSetPos = output.indexOf("bool m_nameHasBeenSet = false;"); @@ -426,9 +419,8 @@ private static StructureShape myShape(Model model) { @Test void injectedResponseMetadata_inStructure_omitsGetter_whileOthersKeepIt() { - // The injected ResponseMetadata envelope is always present -> no HasBeenSet getter. A - // modeled @required member is NOT special-cased (C2J clears required), so it keeps its - // getter just like a plain member. + // The injected ResponseMetadata envelope is always present -> no HasBeenSet getter. A modeled + // @required member is not special-cased, so it keeps its getter like a plain member. Model model = responseMetadataModel(); CppWriter writer = new CppWriter(); MemberRenderer.forStructure(model, myShape(model), "MyShape").renderPublicAccessors(writer); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java index 0ff4553eb25..993a727d05d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java @@ -22,42 +22,28 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * End-to-end guard for suppressing DynamoDB's {@code AttributeValue} union from the default - * sub-object set. Suppression is now driven by {@link DynamoDbTransforms} marking the shape - * {@code @customRendered} and {@link ShapeClassifier} skipping marked shapes — the generic - * {@code ModelGenerator} no longer knows about dynamodb. This test therefore applies the DynamoDB - * transform to its model before running {@code ModelGenerator}, exercising the whole chain: - * transform-marks -> classifier-skips -> single bespoke file. - * - *

The suppression is load-bearing, not cosmetic: {@code CppWriterDelegator.useFileWriter} - * keys writers by filename via {@code computeIfAbsent}, so if {@code AttributeValue} were left in - * {@code subObjects}, {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.SubObjectRenderer} + * End-to-end guard that DynamoDB's {@code AttributeValue} union is suppressed from the default + * sub-object set: {@link DynamoDbTransforms} marks it {@code @customRendered} and + * {@link ShapeClassifier} skips it. Load-bearing, not cosmetic: {@code CppWriterDelegator} keys + * writers by filename, so a leftover {@code AttributeValue} would make + * {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.SubObjectRenderer} * and {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.DynamoDbRenderer} - * would both resolve the same {@code include/aws/dynamodb/model/AttributeValue.h} key, share one - * writer, and APPEND — silently concatenating the generic tagged-union struct onto the bespoke - * document type with no error. This test runs {@code ModelGenerator} end-to-end and asserts the - * emitted header is the bespoke class only, never the generic union. The per-renderer tests - * ({@code DynamoDbRendererTest}, {@code SubObjectRendererTest}) do not exercise this wiring where - * the double-emit would occur. + * share one writer and APPEND, silently concatenating the generic union onto the bespoke type. This + * runs {@code ModelGenerator} end-to-end and asserts the header is the bespoke class only. */ class ModelGeneratorTest { private static final String ATTRIBUTE_VALUE_HEADER = "include/aws/dynamodb/model/AttributeValue.h"; - // A synthetic union member whose generic per-member accessor (produced by SubObjectRenderer) - // does not exist anywhere in the bespoke AttributeValue resource, so its presence/absence - // cleanly distinguishes generic-union output from the hand-written document type. + // Synthetic union member whose generic accessor exists only in generic-union output, not the + // bespoke AttributeValue — so it distinguishes the two. private static final String GENERIC_UNION_MARKER = "WithSyntheticProbe"; // A member unique to the bespoke hand-written AttributeValue (holds the AttributeValueValue). private static final String BESPOKE_MARKER = "std::shared_ptr m_value;"; /** - * A minimal model with a service, one operation, and an {@code AttributeValue} union - * referenced by the operation input. The union carries a synthetic member so that, were it - * rendered generically, {@link #GENERIC_UNION_MARKER} would appear in the output. - * - *

The service carries a {@code ServiceTrait} whose {@code sdkId} is {@code smithyServiceName} - * so {@link DynamoDbTransforms}' own self-guard (which reads the service's sdk id) fires - * consistently with the {@code smithyServiceName} passed to {@code ModelGenerator}. + * Minimal model with a service, one operation, and an {@code AttributeValue} union (carrying a + * synthetic member) referenced by the operation input. The service's {@code ServiceTrait} sdkId + * is {@code smithyServiceName} so {@link DynamoDbTransforms}' self-guard fires consistently. */ private static Model model(String smithyServiceName) { StringShape str = StringShape.builder().id("com.amazonaws.dynamodb#Str").build(); @@ -95,9 +81,8 @@ private static MockManifest generate(String smithyServiceName, String namespace, Model model = model(smithyServiceName); ServiceShape service = model.expectShape( ShapeId.from("com.amazonaws.dynamodb#DynamoDB_20120810"), ServiceShape.class); - // Apply the DynamoDB service-level transform first, mirroring the real ModelCodegenPlugin - // pipeline: for dynamodb it marks AttributeValue @customRendered; for any other service it - // is a no-op. Suppression then flows through ShapeClassifier, not ModelGenerator. + // Apply the DynamoDB service-level transform first (mirrors ModelCodegenPlugin): marks + // AttributeValue @customRendered for dynamodb, no-op otherwise. Suppression flows through ShapeClassifier. Model transformed = DynamoDbTransforms.asTransform().apply(model, service); MockManifest manifest = new MockManifest(); CppWriterDelegator delegator = new CppWriterDelegator(manifest); @@ -127,8 +112,7 @@ void dynamoDb_emitsBespokeAttributeValueOnly_notGenericUnion() { // It is the bespoke document type ... assertTrue(header.contains(BESPOKE_MARKER), "AttributeValue.h must be the bespoke document type: " + header); - // ... and NOT the generic tagged-union SubObjectRenderer would produce for the union - // members. Its presence would mean the generic body was (also) written to this file. + // ... and NOT the generic tagged-union (its presence would mean the generic body was appended here). assertFalse(header.contains(GENERIC_UNION_MARKER), "AttributeValue.h must not contain generic-union accessors (double-emit/corruption): " + header); @@ -147,9 +131,8 @@ void dynamoDb_emitsBespokeAttributeValueOnly_notGenericUnion() { @Test void otherService_rendersAttributeValueUnionGenerically() { - // Control: the suppression is dynamodb-specific. For any other service, DynamoDbRenderer is - // a no-op and the AttributeValue union flows through SubObjectRenderer as a generic union, - // proving the buildRenderers filter is what removes it for dynamodb. + // Control: suppression is dynamodb-specific. For any other service the AttributeValue union + // flows through SubObjectRenderer as a generic union. MockManifest manifest = generate("kinesis", "Kinesis", "AWS_KINESIS_API"); String header = manifest.getFileString("include/aws/kinesis/model/AttributeValue.h") diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/OutgoingEventStreamRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/OutgoingEventStreamRendererTest.java index 483d2f652a2..36373f8b21b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/OutgoingEventStreamRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/OutgoingEventStreamRendererTest.java @@ -29,11 +29,7 @@ class OutgoingEventStreamRendererTest { - /** - * A bidirectional operation whose input binds a @streaming union with one event member whose - * payload is a nested structure (the implicit-structure case, like bedrock's - * BidirectionalInputPayloadPart). - */ + /** Input binds a @streaming union with one event member whose payload is a nested structure (implicit-structure case). */ private static Model structurePayloadModel() { StringShape str = StringShape.builder().id("com.example#String").build(); BlobShape blob = BlobShape.builder().id("com.example#PartBody").build(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java index e4b8e298ba1..4709a4bf4a6 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java @@ -28,13 +28,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Characterization tests pinning the exact generated C++ text for every supported - * protocol, so that changes to the model renderers or their {@link ProtocolTraits} - * strategies cannot silently alter generated output. - * - *

A failure here means generated output changed. Unless the change is intentional, - * fix the production code rather than the assertion; if it is intentional, update the - * assertion in the same commit that changes the renderer. + * Characterization tests pinning the exact generated C++ text for every supported protocol, so + * changes to the renderers or their {@link ProtocolTraits} strategies cannot silently alter output. + * A failure means generated output changed: fix the production code, or update the assertion in the + * same commit if the change is intentional. */ class ProtocolTraitsCharacterizationTest { @@ -72,8 +69,7 @@ private static Model modelFor(Protocol p) { .id("com.example#Nested") .addMember("value", str.getId()) .build(); - // Input carries a plain member, an httpHeader member, and an httpQuery member so - // both request Axis-1 predicates (header + query) fire. + // Input carries plain + httpHeader + httpQuery members so both request predicates fire. StructureShape.Builder inputBuilder = StructureShape.builder() .id("com.example#DoThingInput") .addMember("name", str.getId()) @@ -93,9 +89,8 @@ private static Model modelFor(Protocol p) { .id("com.example#DoThingOutput$status").target(intShape.getId()) .addTrait(new software.amazon.smithy.model.traits.HttpResponseCodeTrait()).build()) .build(); - // SupportsPresigningTransform stamps every query/ec2 OPERATION; mirror it in the fixture so - // this end-to-end characterization pins the same post-transform output (protected - // DumpBodyToUrl decl + protocol-specific impl). + // SupportsPresigningTransform stamps every query/ec2 operation; mirror it so this pins the + // same post-transform output (protected DumpBodyToUrl decl + protocol-specific impl). OperationShape.Builder opBuilder = OperationShape.builder() .id("com.example#DoThing") .input(input.getId()) @@ -500,8 +495,7 @@ private static String stripIndent(String s) { @EnumSource(value = Protocol.class, names = {"QUERY_XML", "EC2"}) void queryLikeResultHeader_noBlankBeforeHttpResponseCodeWhenNoRequestId(Protocol p) { // Without a top-level m_requestId, the last data member is followed directly by - // m_HttpResponseCode (no intervening blank line), matching C2J. The fixture's last - // result member is the @httpResponseCode int "status". + // m_HttpResponseCode (no blank line), matching C2J. String h = stripIndent(file(p, "DoThingResult.h")); assertTrue(h.contains("int m_status{0};\nAws::Http::HttpResponseCode m_HttpResponseCode;"), h); } @@ -518,9 +512,8 @@ void nonQueryResultHeader_blankLinePrecedesRequestId(Protocol p) { @ParameterizedTest @EnumSource(value = Protocol.class, names = {"QUERY_XML", "EC2"}) void queryLikeResultHeader_omitsAWSStringWhenNoStringMemberOrRequestId(Protocol p) { - // With no top-level m_requestId and no string-typed member, the Query/EC2 result header - // has no Aws::String use and must not include AWSString.h, matching C2J include hygiene. - // (The fixture DoThingOutput has only a nested struct + httpResponseCode int member.) + // With no top-level m_requestId and no string-typed member, the Query/EC2 result header has + // no Aws::String use and must not include AWSString.h, matching C2J. String h = file(p, "DoThingResult.h"); assertFalse(h.contains("AWSString.h"), h); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java index 10638077b1f..4622ddb31d8 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -162,11 +162,7 @@ private static Model supportsPresigningModel(boolean marked) { return Model.builder().addShapes(str, input, output, op, service).build(); } - /** - * A presignable operation whose input is the shared {@code smithy.api#Unit} (no {@code input(...)} - * set) — the case that broke: the input shape cannot carry the trait, but the operation can, so - * the decl must still be emitted. Mirrors an IAM-style query op like {@code GetAccountSummary}. - */ + /** Presignable op with a shared {@code smithy.api#Unit} input: the input can't carry the trait but the operation can, so the decl must still emit (IAM-style query op). */ private static Model supportsPresigningUnitInputModel() { StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape output = StructureShape.builder() @@ -177,9 +173,8 @@ private static Model supportsPresigningUnitInputModel() { .build(); ServiceShape service = ServiceShape.builder().id("com.example#Example") .version("2024-01-01").addOperation(op.getId()).build(); - // Assemble (not Model.builder) so the smithy.api#Unit prelude shape exists: the operation has - // no input, so its input target defaults to Unit, which ShapeClassifier resolves to build the - // request. Unit must be present in the model for the request class to be emitted. + // Assemble (not Model.builder) so the smithy.api#Unit prelude shape exists: the op defaults + // its input to Unit, which must be present for the request class to be emitted. return Model.assembler().addShapes(str, output, op, service).assemble().unwrap(); } @@ -229,9 +224,8 @@ void streamingResponseRequest_hasEventStreamAugmentation() { "Missing decoder include: " + h); assertFalse(h.contains("IsEventStreamRequest"), "Response-only op must not declare IsEventStreamRequest: " + h); - // Mainline ordering: handler/decoder sit AFTER the data members and BEFORE the - // HasBeenSet flags (not at the top of the private block). Target the member - // DECLARATION ("DoStreamHandler m_handler;"), not the public getter body. + // Mainline ordering: handler/decoder sit after data members and before HasBeenSet flags. + // Target the member DECLARATION ("DoStreamHandler m_handler;"), not the public getter body. int dataMember = h.indexOf("Aws::String m_name;"); int handlerDecl = h.indexOf("DoStreamHandler m_handler;"); int firstFlag = h.indexOf("HasBeenSet = false;"); @@ -252,10 +246,8 @@ void bidirectionalRequest_alsoHasIsEventStreamRequest() { @Test void bidirectionalRequest_rendersEventStreamInputMemberAsSharedPtr() { - // C2J renders a request with an event-stream (input) member specially: an inline empty - // SerializePayload, a GetBody() override returning the encoded IOStream, and the member - // itself as a std::shared_ptr with a collision-renamed getter (GetMemberBody, - // because GetBody is reserved). The member has no templated setter and stores a shared_ptr. + // C2J renders an event-stream (input) member specially: inline empty SerializePayload, a + // GetBody() override, and the member as shared_ptr with a renamed getter (GetMemberBody). String h = renderRequestHeaderForStreamingOp(true, true); assertTrue(h.contains("#include "), "Missing include: " + h); assertTrue(h.contains("Aws::String SerializePayload() const override { return {}; }"), @@ -281,15 +273,13 @@ void bidirectionalRequest_rendersEventStreamInputMemberAsSharedPtr() { @Test void bidirectionalRequestSource_definesGetBody() { - // The header declares `std::shared_ptr GetBody() const override;`, so the - // source MUST define it (returning the event-stream member) or linking fails. Matches C2J - // StreamRequestSource.vm. + // Header declares GetBody() const override, so the source MUST define it (returning the + // event-stream member) or linking fails. Matches C2J StreamRequestSource.vm. String c = renderStreamingOp(true, true, "DoStreamRequest.cpp"); assertTrue(c.contains( "std::shared_ptr DoStreamRequest::GetBody() const { return m_body; }"), "Bidirectional request source must define GetBody() returning the event-stream member: " + c); - // C2J's event-stream request source pulls AmazonWebServiceResult.h and the Stream/Aws - // usings rather than the JSON serde header. + // C2J's event-stream request source pulls AmazonWebServiceResult.h + Stream/Aws usings, not JSON serde. assertTrue(c.contains("#include "), c); assertTrue(c.contains("using namespace Aws::Utils::Stream;"), c); } @@ -324,10 +314,8 @@ private static Model queryMemberModel() { } /** - * Request headers must NOT include {@code } even when they declare - * URI-taking methods: the base {@code AmazonWebServiceRequest.h} forward-declares - * {@code Aws::Http::URI}, which suffices for a reference parameter, and C2J omits the - * include. Emitting it would break byte-parity with the C2J output. + * Request headers must NOT include {@code }: the base + * {@code AmazonWebServiceRequest.h} forward-declares {@code Aws::Http::URI}, and C2J omits it. */ @Test void requestWithQueryMember_doesNotIncludeUriHeader() { @@ -387,10 +375,8 @@ private static String renderRawStreamingPayloadRequest(String fileSuffix) { @Test void rawStreamingPayloadRequest_usesStreamingBaseClassAndDropsPayloadMembers() { - // C2J: a request with a raw streaming @httpPayload member derives from - // StreamingRequest, which supplies GetBody/SetBody and GetContentType/SetContentType. - // The payload member (body) and the contentType member are stripped, and SerializePayload - // is not emitted (the base handles the body). Other members (modelId) remain. + // C2J: a raw streaming @httpPayload request derives from StreamingRequest (supplies + // GetBody/GetContentType); the body + contentType members and SerializePayload are dropped. String h = renderRawStreamingPayloadRequest("DoStreamRequest.h"); assertTrue(h.contains("class DoStreamRequest : public StreamingExampleRequest {"), "Must derive from StreamingExampleRequest: " + h); @@ -428,11 +414,7 @@ void rawStreamingPayloadRequestSource_usesStreamIncludesNotJsonSerde() { "Streaming-payload request source must not use the Json namespace: " + c); } - /** - * Same as {@link #rawStreamingPayloadRequestModel()} but under REST-JSON, where {@code contentType} - * (stripped, supplied by the streaming base) is the ONLY header-bound member and - * {@code hasTargetHeader()} is false. This is the combination that exposes header/source drift. - */ + /** Like {@link #rawStreamingPayloadRequestModel()} but REST-JSON, where the stripped {@code contentType} is the only header-bound member ({@code hasTargetHeader()} false) — exposes header/source drift. */ private static Model rawStreamingPayloadRestJsonRequestModel() { StringShape str = StringShape.builder().id("com.example#String").build(); software.amazon.smithy.model.shapes.BlobShape blob = @@ -477,11 +459,7 @@ private static String renderRawStreamingPayloadRestJsonRequest(String fileSuffix // --- @httpChecksum --- - /** - * A JSON operation with @httpChecksum. The input carries a {@code checksumAlgorithm} enum member - * (for requestAlgorithmMember) and a {@code checksumMode} enum member (for - * requestValidationModeMember); the trait is configured from the given values. - */ + /** JSON op with @httpChecksum; input carries {@code checksumAlgorithm} and {@code checksumMode} enum members. */ private static Model httpChecksumModel(software.amazon.smithy.aws.traits.HttpChecksumTrait trait) { StringShape str = StringShape.builder().id("com.example#String").build(); software.amazon.smithy.model.shapes.EnumShape algo = @@ -575,10 +553,8 @@ void httpChecksumRequestChecksumRequired_rendersInlineOverride() { @Test void httpChecksumRequired_rendersInlineShouldComputeContentMd5() { - // The legacy smithy.api#httpChecksumRequired trait (s3control uses it) requests a - // Content-MD5 header. C2J derives Shape.computeContentMd5 from it and emits an inline - // ShouldComputeContentMd5() override (RequestHeader.vm:105-108, no .cpp body). Distinct - // from the flexible @httpChecksum trait. + // The legacy smithy.api#httpChecksumRequired trait (s3control) emits an inline + // ShouldComputeContentMd5() override (no .cpp body); distinct from the flexible @httpChecksum. StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape input = StructureShape.builder().id("com.example#PutThingInput") .addMember("name", str.getId()).build(); @@ -628,11 +604,7 @@ void requestWithoutHttpChecksum_emitsNoChecksumMethods() { // --- @requestCompression --- - /** - * Model for a JSON operation carrying {@code @requestCompression(encodings: ["gzip"])} on - * either a plain input (streaming = false) or a raw {@code @httpPayload} blob body input - * (streaming = true). - */ + /** JSON op with {@code @requestCompression(encodings: ["gzip"])} on a plain input (streaming=false) or a raw {@code @httpPayload} blob body (streaming=true). */ private static Model requestCompressionModel(boolean streaming, java.util.List encodings) { StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape.Builder input = StructureShape.builder().id("com.example#PutThingInput"); @@ -683,10 +655,8 @@ private static String renderCompressionRequest(boolean streaming, String fileSuf @Test void requestCompressionGzip_headerDeclaresGuardedVirtualOverride() { - // C2J's RequestHeader.vm:148-156 emits GetSelectedCompressionAlgorithm as a virtual override - // gated by ENABLED_ZLIB_REQUEST_COMPRESSION. The types (CompressionAlgorithm, - // RequestCompressionConfig) come from the base AmazonWebServiceRequest.h transitively — - // NO extra include in the request header. + // C2J emits GetSelectedCompressionAlgorithm as a virtual override gated by + // ENABLED_ZLIB_REQUEST_COMPRESSION; its types come from the base transitively (no extra include). String h = renderCompressionRequest(false, "PutThingRequest.h"); assertTrue(h.contains("#ifdef ENABLED_ZLIB_REQUEST_COMPRESSION"), "Missing ENABLED_ZLIB_REQUEST_COMPRESSION guard: " + h); @@ -699,10 +669,8 @@ void requestCompressionGzip_headerDeclaresGuardedVirtualOverride() { @Test void requestCompressionGzip_nonStreamingSourceUsesBodySizeCheck() { - // Non-streaming variant (ModelClassRequiredCompression.vm): DISABLE -> NONE, then read the - // already-serialized body via AmazonSerializableWebServiceRequest::GetBody(), compare its - // size to config.requestMinCompressionSizeBytes, and either NONE or GZIP. Matches cloudwatch - // PutMetricDataRequest.cpp exactly. Body only touches base state; NOT serde-blocked. + // Non-streaming variant: DISABLE -> NONE, else compare the serialized body size to + // config.requestMinCompressionSizeBytes to pick NONE or GZIP. Matches cloudwatch PutMetricDataRequest.cpp. String c = renderCompressionRequest(false, "PutThingRequest.cpp"); assertTrue(c.contains("#ifdef ENABLED_ZLIB_REQUEST_COMPRESSION"), c); assertTrue(c.contains( @@ -772,12 +740,9 @@ void requestWithoutCompressionTrait_emitsNothingCompressionRelated() { @Test void rawStreamingPayloadRequestRestJson_headerAndSourceAgreeOnRequestSpecificHeaders() { - // Under a REST protocol (hasTargetHeader() == false), a raw-streaming-payload request whose - // ONLY header-bound member is the stripped contentType must emit GetRequestSpecificHeaders in - // NEITHER the header nor the source. The header renders from the contentType-excluded shape; - // the source MUST render from the same shape. Otherwise the source defines - // GetRequestSpecificHeaders() out-of-line for a method the header never declares (a C++ - // compile error). + // Under REST (hasTargetHeader()==false), a raw-streaming-payload request whose only + // header-bound member is the stripped contentType must emit GetRequestSpecificHeaders in neither + // header nor source, else the source defines a method the header never declares (compile error). String h = renderRawStreamingPayloadRestJsonRequest("DoStreamRequest.h"); String c = renderRawStreamingPayloadRestJsonRequest("DoStreamRequest.cpp"); assertFalse(h.contains("GetRequestSpecificHeaders"), @@ -831,9 +796,8 @@ private static String renderOperationContextRequest(Model model, String fileSuff @Test void operationContextParams_headerDeclaresGetters() { - // An operation carrying only smithy.rules#operationContextParams must produce both the - // GetEndpointContextParams() virtual override and the GetOperationContextParams() accessor - // in its request header. Fails on main because RequestRenderer ignores the trait. + // An op carrying only smithy.rules#operationContextParams must emit both GetEndpointContextParams() + // and GetOperationContextParams() in its request header. String h = renderOperationContextRequest(operationContextParamsOnlyModel(), "DoBatchRequest.h"); assertTrue(h.contains("EndpointParameters GetEndpointContextParams() const override;"), "Missing GetEndpointContextParams decl: " + h); @@ -983,11 +947,7 @@ void operationContextParams_multiSelectFlattenPattern_endToEnd() { // --- aws.auth#unsignedPayload (SignBody) --- - /** - * Operation carrying {@code aws.auth#unsignedPayload}. When {@code emptyInput} is false the input - * has a member; when true the input has none (exercises the {@code !members.isEmpty()} guard). - * When {@code marked} is false the trait is omitted. - */ + /** Op with {@code aws.auth#unsignedPayload} ({@code marked}); {@code emptyInput} toggles a member to exercise the {@code !members.isEmpty()} guard. */ private static Model unsignedPayloadModel(boolean marked, boolean emptyInput) { StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape.Builder inB = StructureShape.builder().id("com.example#DoThingRequest"); @@ -1089,9 +1049,8 @@ private static Model longPollingModel(boolean marked) { @Test void longPollingTrait_emitsIsLongPollingOperationTrue() { - // C2J RequestHeader.vm emits IsLongPollingOperation() -> true for a long-polling request - // (gated on $operation.longPolling); the marker (stamped by LongPollingTransform) drives the - // same override here. + // C2J emits IsLongPollingOperation() -> true for a long-polling request; the marker (stamped + // by LongPollingTransform) drives the same override here. String h = renderDoThingRequestHeader(longPollingModel(true)); assertTrue(h.contains("bool IsLongPollingOperation() const override { return true; }"), "LongPollingTrait must emit the IsLongPollingOperation override: " + h); @@ -1100,8 +1059,7 @@ void longPollingTrait_emitsIsLongPollingOperationTrue() { @Test void longPollingTrait_emittedWithTopIdentityMethods() { // Ordering: IsLongPollingOperation sits with the top identity methods (after - // GetServiceRequestName, before SerializePayload), NOT down with the SignBody/IsChunked - // block. Matches C2J RequestHeader.vm lines 57-64. + // GetServiceRequestName, before SerializePayload), not the SignBody/IsChunked block. String h = renderDoThingRequestHeader(longPollingModel(true)); int requestName = h.indexOf("GetServiceRequestName"); int longPolling = h.indexOf("IsLongPollingOperation"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java index 7f01064501c..c543526c953 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java @@ -92,8 +92,7 @@ void ec2Result_usesResponseSuffix() { @Test void cborResult_omitsHasBeenSetAccessors() { // C2J's CborResultHeader.vm sets useRequiredField=false, so result classes never emit - // HasBeenSet() accessors — same as every other protocol. (Only sub-object and request - // headers set useRequiredField=true.) + // HasBeenSet() accessors (only sub-object and request headers do). String h = renderResultHeader(software.amazon.smithy.protocol.traits.Rpcv2CborTrait.builder().build()); assertFalse(h.contains("HasBeenSet() const"), h); } @@ -306,9 +305,8 @@ void statusCodeMember_setFromResponseCode() { } /** - * A one-member rest-xml output operation whose output structure optionally carries the internal - * {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.TopLevelHostIdTrait} - * marker (as {@code S3ControlTransforms} stamps it). + * A one-member rest-xml output whose output structure optionally carries the internal + * {@link com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.TopLevelHostIdTrait} marker. */ private static Model hostIdResultModel(boolean marked) { StringShape str = StringShape.builder().id("com.example#Str").build(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ServiceNameUtilTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ServiceNameUtilTest.java index ecbf42c2093..9d8e5ba74de 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ServiceNameUtilTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ServiceNameUtilTest.java @@ -32,14 +32,12 @@ void capitalize_singleChar() { @Test void getExportMacro_standardService() { - // Test the export macro format follows AWS_{SERVICE_NAME}_API pattern - // We test it indirectly through the capitalize logic it uses + // Export macro is AWS_{SERVICE_NAME}_API; exercised indirectly via the capitalize logic it uses. assertEquals("Kinesis", ServiceNameUtil.capitalize("kinesis")); } @Test void getProjectName_standardService() { - // Test the project name format (lowercase hyphenated) - // This would require a full ServiceShape which is tested separately + // Project name format (lowercase hyphenated) needs a full ServiceShape; covered separately. } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java index 42517bc1492..58526386d4a 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifierTest.java @@ -424,13 +424,7 @@ void noInputOperation_stillProducesRequest() { "Expected a RequestInfo for the no-input operation Ping"); } - /** - * A structure that is BOTH an operation output AND referenced as a member (via a list - * element). Mirrors Lambda's FunctionConfiguration, which is the output of - * GetFunctionConfiguration and also the element of FunctionList / a member of another - * response. Such a dual-role shape must end up in BOTH results (per-op output) and - * subObjects (standalone model file), matching C2J. - */ + /** Structure that is both an operation output and a list-element member (dual-role, like Lambda FunctionConfiguration); must end up in both results and subObjects. */ private Model buildDualRoleModel() { StringShape str = StringShape.builder().id("com.example#String").build(); // Thing is the output of GetThing AND the element of ThingList. @@ -503,12 +497,7 @@ void outputOnly_appearsInResultsButNotSubObjects() { "Output-only GetItemResponse must NOT be over-emitted as a sub-object: " + classified.subObjects()); } - /** - * A @streaming union with two event members: one whose sole payload is an @eventPayload blob - * (like Lambda's InvokeResponseStreamUpdate / the PayloadChunk member) and one with only a - * plain string member (like a CompleteEvent). Bound to an operation output so both event - * structs are reachable. - */ + /** @streaming union with an @eventPayload-blob event and a plain-string event, bound to an operation output so both are reachable. */ private Model eventStreamBlobPayloadModel() { StringShape str = StringShape.builder().id("com.example#String").build(); BlobShape blob = BlobShape.builder().id("com.example#Blob").build(); @@ -571,12 +560,7 @@ void nonBlobEvent_staysSubObject() { "Non-blob event struct must NOT be a blob-payload event: " + classified.blobPayloadEvents()); } - /** - * A service whose operation input references two structs: one marked {@link CustomRenderedTrait} - * (owned by a dedicated renderer) and one plain. The classifier's generic marker rule must drop - * the marked one from subObjects while keeping the plain one — for any service, not just - * dynamodb. - */ + /** Operation input references two structs, one marked {@link CustomRenderedTrait} and one plain; the marker rule drops the marked one from subObjects for any service. */ private Model modelWithCustomRenderedShape() { StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape marked = StructureShape.builder() @@ -617,12 +601,7 @@ void customRenderedShape_isExcludedFromSubObjects() { "unmarked shape must remain a sub-object: " + classified.subObjects()); } - /** - * A @streaming union with two event members: one empty-member event (like S3's - * ContinuationEvent / EndEvent, referenced by nothing after EventStreamRenderer's void() - * callback fix) and one data event carrying a string member. Bound to an operation output so - * both event structs are reachable. - */ + /** @streaming union with an empty-member event (like S3 ContinuationEvent) and a string data event, bound to an operation output so both are reachable. */ private Model eventStreamEmptyAndDataModel() { StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape emptyEvt = StructureShape.builder() @@ -668,9 +647,8 @@ void classifyDropsEmptyMemberEventStructFromSubObjects() { @Test void classifyDropsIncomingEventStreamUnionFromSubObjects() { - // The @streaming union bound to an operation output (an incoming event stream) is realized - // via the generated handler; nothing references the union as a data type, so it must not be - // emitted as a standalone sub-object header. + // The @streaming union bound to an output (incoming event stream) is realized via the + // generated handler; nothing references it as a data type, so no standalone sub-object header. Model model = eventStreamEmptyAndDataModel(); ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); var classified = ShapeClassifier.classify(model, service, ProtocolResolver.resolve(service, model)); @@ -681,9 +659,8 @@ void classifyDropsIncomingEventStreamUnionFromSubObjects() { @Test void deprecatedOperation_inputAndOutputExcludedFromRequestsAndResults() { - // Legacy C2J drops @deprecated operations entirely, so their orphaned request/result structs - // are never emitted. The classifier must not put a @deprecated op's input in requests nor its - // output in results, while a live op's input/output are present. + // Legacy C2J drops @deprecated operations entirely; the classifier must keep a @deprecated + // op's input/output out of requests/results while a live op's are present. StringShape str = StringShape.builder().id("com.example#String").build(); StructureShape deprecatedRequest = StructureShape.builder() .id("com.example#DeprecatedRequest").addMember("id", str.getId()).build(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java index 6bd4b31f34e..01bfb42a133 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java @@ -26,19 +26,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Verifies that {@link SubObjectRenderer} renders C2J-style union shapes. In C2J a union is a - * {@code structure} with {@code "union": true} and is emitted by the same ModelClass templates - * as a plain structure (serde decls + per-member Get/Set/With/HasBeenSet accessors + private - * data members and flags). Non-streaming unions must produce header + source files; {@code - * @streaming} unions belong to the event-stream renderers and must be skipped here. + * Verifies {@link SubObjectRenderer} renders C2J-style union shapes like plain structures (serde + * decls + per-member accessors + private members). Non-streaming unions produce header + source; + * {@code @streaming} unions belong to the event-stream renderers and are skipped here. */ class SubObjectRendererTest { - /** - * A model with a plain structure sub-object, a non-streaming union, and a {@code @streaming} - * union — mirroring bedrock-runtime (ContentBlock / ToolChoice are data unions; - * InvokeModelWithBidirectionalStreamInput is a streaming union). - */ + /** Plain structure sub-object, a non-streaming data union, and a {@code @streaming} union (mirrors bedrock-runtime). */ private static Model model() { StringShape str = StringShape.builder().id("com.example#Str").build(); StructureShape leaf = StructureShape.builder() @@ -59,13 +53,11 @@ private static Model model() { .addTrait(new StreamingTrait()) .addMember("chunk", leaf.getId()) .build(); - // Memberless structure (e.g. bedrock-runtime AnyToolChoice / AutoToolChoice): C2J emits - // no private: section when the shape has no members. + // Memberless structure (e.g. bedrock-runtime AnyToolChoice): C2J emits no private: section. StructureShape empty = StructureShape.builder() .id("com.example#AnyToolChoice") .build(); - // Union with a blob member (e.g. bedrock-runtime AudioSource): its source needs - // HashingUtils.h for Base64 blob serde. + // Union with a blob member (e.g. bedrock-runtime AudioSource): source needs HashingUtils.h for Base64 serde. software.amazon.smithy.model.shapes.BlobShape blob = software.amazon.smithy.model.shapes.BlobShape.builder().id("com.example#PartBody").build(); UnionShape blobUnion = UnionShape.builder() @@ -120,7 +112,6 @@ void nonStreamingUnion_headerHasSerdeDeclsAndPerMemberAccessors() { assertTrue(h.contains("AWS_EXAMPLE_API ContentBlock() = default;"), h); assertTrue(h.contains("AWS_EXAMPLE_API ContentBlock(Aws::Utils::Json::JsonView jsonValue);"), h); assertTrue(h.contains("AWS_EXAMPLE_API Aws::Utils::Json::JsonValue Jsonize() const;"), h); - // Per-member accessors for each variant. assertTrue(h.contains("GetText") && h.contains("SetText") && h.contains("WithText"), h); assertTrue(h.contains("GetLeaf") && h.contains("SetLeaf") && h.contains("WithLeaf"), h); assertTrue(h.contains("bool m_textHasBeenSet = false;"), h); @@ -140,8 +131,7 @@ void nonStreamingUnion_sourceHasSerdeImpls() { @Test void blobMemberSource_includesHashingUtils() { - // A sub-object with a blob member needs HashingUtils.h in its source (Base64 blob serde), - // matching C2J's computeSourceIncludes. The header must NOT carry it (source-only include). + // Blob member needs HashingUtils.h in source (Base64 serde, C2J computeSourceIncludes); header must not carry it. java.util.Map files = renderAll(); String c = files.get("AudioSource.cpp"); assertTrue(c.contains("#include "), @@ -153,9 +143,8 @@ void blobMemberSource_includesHashingUtils() { @Test void memberlessShape_omitsPrivateSection() { - // C2J emits the private: section only when the shape has members - // (ModelClassMembersAndInlines.vm: `#if($shape.members.size() > 0 ...`). A memberless - // sub-object ends right after its serde decls — no trailing blank line and no private:. + // C2J emits private: only when the shape has members; a memberless sub-object ends right + // after its serde decls with no private: section. String h = renderAll().get("AnyToolChoice.h"); assertTrue(h.contains("AWS_EXAMPLE_API Aws::Utils::Json::JsonValue Jsonize() const;"), h); assertFalse(h.contains("private:"), @@ -173,17 +162,12 @@ void streamingUnion_isNotRenderedBySubObjectRenderer() { // --- dual-role (operation output that is also a member) requestId stamp --- - /** - * A model where {@code Thing} is BOTH the output of {@code DoThing} AND a member of {@code Plain} - * (dual-role: an output referenced as a member). {@code Plain} is a plain member-only sub-object. - * The service carries the given protocol trait. - */ + /** {@code Thing} is both the output of {@code DoThing} and a member of {@code Plain} (dual-role); {@code Plain} is member-only. */ private static Model dualRoleModel(Trait protocolTrait) { StringShape str = StringShape.builder().id("com.example#Str").build(); StructureShape thing = StructureShape.builder() .id("com.example#Thing").addMember("name", str.getId()).build(); - // Plain references Thing (making Thing a member target) and is itself a member of the input, - // so both Plain and Thing are reachable sub-objects; only Thing is an operation output. + // Both Plain and Thing are reachable sub-objects; only Thing is an operation output. StructureShape plain = StructureShape.builder() .id("com.example#Plain") .addMember("label", str.getId()) @@ -241,8 +225,7 @@ void memberOnlySubObject_jsonProtocol_hasNoRequestId() { @Test void dualRoleOutput_queryProtocol_hasNoRequestId() { - // Query/EC2 dual-role outputs get a ResponseMetadata member instead (injectResponseMetadata), - // so resultHasTopLevelRequestId() is false and no requestId block is stamped here. + // Query/EC2 dual-role outputs get ResponseMetadata instead, so no requestId block is stamped. String h = renderDualRole( new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait()).get("Thing.h"); assertFalse(h.contains("GetRequestId"), diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java index c178d0abb85..04506885b12 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java @@ -54,7 +54,6 @@ void transformReceivesOutputOfPrevious() { .build(); ServiceShape service = original.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); - // First transform adds a new shape Model withExtra = original.toBuilder() .addShape(ServiceShape.builder().id("com.example#Extra").version("2024-01-01").build()) .build(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java index 0a436e66646..1e4755a7763 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/JsonProtocolTraitsTest.java @@ -212,9 +212,8 @@ void restJson_headerMember_isWireSerialized() { @Test void restJson_additionalHeadersTrait_emitsConstantHeaderBeforeMemberHeaders() { - // A streaming request marked with AdditionalRequestHeadersTrait (Glacier's - // x-amz-glacier-version) emits the constant header inside GetRequestSpecificHeaders, - // ordered after any X-Amz-Target and before the member-driven headers (StreamRequestSource.vm). + // A request with AdditionalRequestHeadersTrait (Glacier's x-amz-glacier-version) emits the + // constant header in GetRequestSpecificHeaders, after X-Amz-Target and before member headers. var req = reqWith(true, false).toBuilder() .addTrait(new com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms .AdditionalRequestHeadersTrait(java.util.Map.of("x-amz-glacier-version", "2012-06-01"))) @@ -247,9 +246,8 @@ void restJson_queryMember_isWireSerialized() { @Test void supportsPresigning_emitsDumpBodyToUrlStubImpl() { - // A presignable operation (Polly SynthesizeSpeech) carries SupportsPresigningTrait on the - // OPERATION; the decl is emitted by RequestRenderer, and JsonProtocolTraits supplies a stub - // impl (gated on the same operation trait) that defers serde. + // A presignable op (Polly SynthesizeSpeech) carries SupportsPresigningTrait; RequestRenderer + // emits the decl, JsonProtocolTraits supplies a stub impl (same trait) that defers serde. var req = reqWith(false, false); var model = modelWith(req); String i = render(w -> restJson.writeRequestMethodImpls( w, "SynthesizeSpeechRequest", req, opDoThingPresigning(), svcAthena(), model)); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java index e9a42e13787..42fe60411dc 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsIncludeSetTest.java @@ -49,9 +49,8 @@ void queryXml_subobjectHeader_hasStreamFwd() { @Test void requestSource_everyProtocolIncludesNumericForListHeaderAccumulate() { - // RequestHeaderSerializer emits std::accumulate for list-typed @httpHeader members and is - // protocol-agnostic, so every protocol's REQUEST_SOURCE must declare . Guards the - // JSON/QueryXml regression where only RestXml carried it (relying on transitive includes). + // RequestHeaderSerializer emits std::accumulate for list-typed @httpHeader members (protocol- + // agnostic), so every protocol's REQUEST_SOURCE must declare . Guards a JSON/QueryXml regression. for (ProtocolTraits t : List.of( new JsonProtocolTraits(Protocol.JSON), new JsonProtocolTraits(Protocol.REST_JSON), diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsSerdeTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsSerdeTest.java index 39a7858d7bd..ae96eb2b59e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsSerdeTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsSerdeTest.java @@ -15,9 +15,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Pins the invariant that the event- and error-payload stubs stay protocol-agnostic - * for EVERY protocol -- they are {@code default} methods on {@link ProtocolTraits} and - * no implementation should override them with protocol-specific text until + * Pins that event- and error-payload stubs stay protocol-agnostic for EVERY protocol: {@code default} + * methods on {@link ProtocolTraits} that no impl overrides with protocol-specific text until * schema-based serde lands. */ class ProtocolTraitsSerdeTest { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsStreamingPayloadTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsStreamingPayloadTest.java index 76be69124d0..5ae6596afae 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsStreamingPayloadTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/ProtocolTraitsStreamingPayloadTest.java @@ -21,11 +21,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * C2J gates {@code SerializePayload()} on {@code !hasStreamMembers()} in the single shared - * {@code RequestHeader.vm}, so the rule is protocol-agnostic: no protocol emits SerializePayload - * for a request with a raw streaming {@code @httpPayload} member. These tests pin that every - * {@link ProtocolTraits} implementation honours it (and still emits SerializePayload for a plain - * request). + * C2J gates {@code SerializePayload()} on {@code !hasStreamMembers()}, so the rule is protocol-agnostic: + * no protocol emits it for a raw streaming {@code @httpPayload} request. Pins that every + * {@link ProtocolTraits} honours this (and still emits SerializePayload for a plain request). */ class ProtocolTraitsStreamingPayloadTest { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java index ee64dda0c18..a9778cae485 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/protocol/XmlProtocolTraitsTest.java @@ -24,10 +24,8 @@ private static String render(java.util.function.Consumer body) { } /** - * Renders inside a simulated class body (indent level 1), matching how request-method - * declarations are emitted by {@code RequestRenderer}. Query/EC2 decls open with a - * {@code dedent()} for the {@code protected:} sandwich, which requires a non-zero - * starting indent. + * Renders at indent level 1, as {@code RequestRenderer} emits request-method decls. Query/EC2 + * decls open with a {@code dedent()} for the {@code protected:} sandwich, needing a non-zero indent. */ private static String renderInClassBody(java.util.function.Consumer body) { CppWriter writer = new CppWriter(); @@ -279,9 +277,8 @@ void restXml_withoutEmbeddedErrorsTrait_omitsHasEmbeddedError() { @Test void queryXml_serializePayloadAndDumpBodyToUrlImpl() { - // The DumpBodyToUrl DECL is emitted protocol-agnostically by RequestRenderer (gated on the - // operation's SupportsPresigningTrait), so the query traits' decls no longer carry it; the - // IMPL is here and now gated on the SAME operation trait so decl+impl stay symmetric. + // DumpBodyToUrl DECL is emitted by RequestRenderer (gated on SupportsPresigningTrait), not the + // query traits; the IMPL is here, gated on the SAME operation trait so decl+impl stay symmetric. var req = reqWith(false, false); var model = modelWith(req); ProtocolTraits q = new QueryXmlProtocolTraits(Protocol.QUERY_XML); String d = renderInClassBody(w -> q.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThingPresigning(), model)); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java index 9de7100e3f7..d0150f6d619 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/endpointcontext/SmithyEndpointsJmesPathVisitorTest.java @@ -143,9 +143,8 @@ void multiSelectListFlattenPattern() { @Test void unsupportedNode_throws() { - // A JMESPath expression exercising an unsupported node (filter projection) must throw - // UnsupportedOperationException, inherited from UnsupportedExpressionVisitor: fail fast - // on unrecognized constructs. + // An unsupported node (filter projection) must throw UnsupportedOperationException (from + // UnsupportedExpressionVisitor): fail fast on unrecognized constructs. StructureShape input = StructureShape.builder() .id("com.example#Req").addMember("x", ShapeId.from(STR)).build(); Model model = Model.builder().addShapes(str(), input).build(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java index 899681f7f22..225c674c4a7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java @@ -20,12 +20,10 @@ /** * Verifies {@link DynamoDbTransforms} stamps {@link CustomRenderedTrait} onto DynamoDB's - * {@code AttributeValue} shape and is a no-op for other services / absent shapes. - * - *

Also serves as the empirical proof that Smithy accepts an in-memory trait instance - * with a synthetic, undefined id ({@code aws.cpp.internal#customRendered}) added inside a transform - * via {@code shapeToBuilder().addTrait(...)} + {@code model.toBuilder().build()} — no model-level - * trait definition required, because that build path does not run trait-definition validation. + * {@code AttributeValue} and is a no-op for other services / absent shapes. Also proves Smithy + * accepts an in-memory trait instance with a synthetic, undefined id via + * {@code shapeToBuilder().addTrait(...)} + {@code model.toBuilder().build()} — that build path skips + * trait-definition validation. */ class DynamoDbTransformsTest { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java index 0f0e352e7c9..442a7188edf 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java @@ -135,7 +135,6 @@ void modelsUserDataAsSensitiveSecureBlobAttributeValue() { assertTrue(valueTarget.hasTrait(SensitiveTrait.class), "the blob must be @sensitive so it renders as CryptoBuffer"); - // UserData is repointed to SecureBlobAttributeValue. MemberShape userData = out.expectShape( ShapeId.from("com.example#ModifyInstanceAttributeRequest"), StructureShape.class) .getAllMembers().get("UserData"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java index 49c31744cf5..def0b08b431 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java @@ -47,7 +47,6 @@ void removesInvokeAsyncOperationAndShapes() { assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsync")).isEmpty()); assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsyncRequest")).isEmpty()); assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsyncResult")).isEmpty()); - // Invoke and its shapes are untouched assertTrue(out.getShape(ShapeId.from("com.example#Invoke")).isPresent()); assertTrue(out.getShape(ShapeId.from("com.example#InvocationRequest")).isPresent()); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 9879cc9a88e..89e52711383 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -103,11 +103,7 @@ void copyObjectResultRename_throwsOnCollision() { assertThrows(IllegalStateException.class, () -> S3Transforms.asTransform().apply(m, svc)); } - /** - * Builds an S3 model with a single PutObject-style operation whose input and output both - * carry an {@code Expires} member (initially a {@code string}, matching the current model), - * mirroring the operation-wiring pattern in {@code AccessAnalyzerTransformsTest}. - */ + /** PutObject-style op whose input and output both carry a {@code string} {@code Expires} member. */ private static Model expiresModel() { Shape expires = StringShape.builder().id(NS + "#Expires").build(); StructureShape input = StructureShape.builder().id(NS + "#PutObjectRequest") @@ -141,7 +137,6 @@ void retypesExpiresShapeToTimestamp() { Model out = S3Transforms.asTransform().apply(m, expiresService(m)); assertTrue(out.expectShape(ShapeId.from(NS + "#Expires")) instanceof TimestampShape, "Expires retyped to a timestamp shape"); - // Both input and output Expires members now target the timestamp. StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); StructureShape output = out.expectShape(ShapeId.from(NS + "#GetObjectOutput"), StructureShape.class); assertTrue(out.expectShape(input.getMember("Expires").orElseThrow().getTarget()) instanceof TimestampShape); @@ -149,9 +144,8 @@ void retypesExpiresShapeToTimestamp() { } /** - * Builds an S3 model with a ListParts-style operation whose request and result reference the - * {@code PartNumberMarker} / {@code NextPartNumberMarker} shapes as strings (matching the current - * Smithy model, where Coral2Smithy treats them as opaque pagination tokens). + * ListParts-style op whose request/result reference {@code PartNumberMarker} / + * {@code NextPartNumberMarker} as strings (Coral2Smithy treats them as opaque pagination tokens). */ private static Model partNumberMarkerModel() { Shape marker = StringShape.builder().id(NS + "#PartNumberMarker").build(); @@ -183,7 +177,6 @@ void retypesPartNumberMarkersToInteger() { "PartNumberMarker retyped to integer to preserve the shipped C2J int API"); assertTrue(out.expectShape(ShapeId.from(NS + "#NextPartNumberMarker")) instanceof IntegerShape, "NextPartNumberMarker retyped to integer to preserve the shipped C2J int API"); - // Request and result members both now resolve to integer targets. StructureShape input = out.expectShape(ShapeId.from(NS + "#ListPartsRequest"), StructureShape.class); StructureShape output = out.expectShape(ShapeId.from(NS + "#ListPartsOutput"), StructureShape.class); assertTrue(out.expectShape(input.getMember("PartNumberMarker").orElseThrow().getTarget()) instanceof IntegerShape); @@ -206,9 +199,8 @@ void marksOverrideStreamingRequests() { } /** - * Builds an S3 model with a PutObject-style request carrying the checksum members plus a - * non-checksum member; {@code withAlgorithmMember} controls whether the request also has the - * {@code ChecksumAlgorithm} member that gates the C2J customization. + * PutObject-style request with checksum members plus a non-checksum one; {@code withAlgorithmMember} + * toggles the {@code ChecksumAlgorithm} member that gates the C2J customization. */ private static Model checksumModel(boolean withAlgorithmMember) { Shape crc32 = StringShape.builder().id(NS + "#ChecksumCRC32").build(); @@ -298,7 +290,6 @@ void doesNotAddExpiresStringToInput() { @Test void appendsMissingBucketLocationConstraintRegions() { ServiceShape svc = s3Service("S3"); - // Model BucketLocationConstraint as an EnumShape with an existing region. software.amazon.smithy.model.shapes.EnumShape enumShape = software.amazon.smithy.model.shapes.EnumShape.builder() .id(NS + "#BucketLocationConstraint") @@ -386,10 +377,7 @@ void injectsGetObjectId2Only() { "ObjectRequestId shape must not be created"); } - /** - * Builds an S3 model with a single operation whose input carries two ordinary members, so the - * appended-last ordering of the injected access-log tag member can be asserted. - */ + /** Op whose input has two ordinary members, so the access-log tag's appended-last order is assertable. */ private static Model accessLogModel() { StructureShape input = StructureShape.builder().id(NS + "#PutObjectRequest") .addMember("Bucket", ShapeId.from("smithy.api#String")) @@ -425,13 +413,11 @@ void injectsCustomizedAccessLogTagIntoRequestAsStringMapAppendedLast() { assertEquals("smithy.api#String", mapShape.getKey().getTarget().toString()); assertEquals("smithy.api#String", mapShape.getValue().getTarget().toString()); - // Must bind to the query string (@httpQueryParams) so RequestBindings.hasQueryStringMembers - // is true and the request emits AddQueryStringParameters — matching C2J, which renders that - // method on every request via the customizedAccessLogTag querystring member. + // Must bind @httpQueryParams so the request emits AddQueryStringParameters — matching C2J, + // which renders that method via the customizedAccessLogTag querystring member. assertTrue(tag.hasTrait(software.amazon.smithy.model.traits.HttpQueryParamsTrait.class), "customizedAccessLogTag must carry @httpQueryParams"); - // Appended after all existing members, preserving prior order. java.util.List order = new java.util.ArrayList<>(input.getAllMembers().keySet()); assertEquals(java.util.List.of("Bucket", "Key", "customizedAccessLogTag"), order, "access-log tag member appended last"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java index 5993373c131..4e41dcdb333 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java @@ -14,7 +14,7 @@ class SourceRegionTransformTest { - // Builds a single-operation service model. sdkId drives ServiceNameUtil.getSmithyServiceName. + // sdkId drives ServiceNameUtil.getSmithyServiceName. private static Model modelWithOp(String sdkId, String opName, String reqName) { StructureShape req = StructureShape.builder() .id("com.example#" + reqName) @@ -57,7 +57,6 @@ void injectsSourceRegionIntoRdsRequest() { @Test void noOpForUntargetedOperation() { - // Operation not in the RDS table -> unchanged Model m = modelWithOp("RDS", "DescribeDBClusters", "DescribeDBClustersRequest"); Model out = SourceRegionTransform.asTransform().apply(m, service(m)); assertTrue(out.expectShape(ShapeId.from("com.example#DescribeDBClustersRequest"), diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java index 849302eebaa..60196800cb1 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java @@ -43,7 +43,7 @@ void addsValuesToEnumShape() { List values = EnumRenderer.getEnumValues( out.expectShape(ShapeId.from("com.example#QueueAttributeName"))); assertTrue(values.containsAll(ADDED)); - assertTrue(values.contains("All")); // originals preserved + assertTrue(values.contains("All")); } @Test diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java index e138b7e54d5..6143b4b6b0c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java @@ -52,9 +52,8 @@ private static Model twoOpModel(Trait protocolTrait, ServiceTrait serviceTrait, } /** - * One operation with a normal input and one operation with NO input (its input target defaults to - * {@code smithy.api#Unit}), under a service carrying {@code protocolTrait}. Mirrors an IAM-style - * {@code GetAccountSummary} where the request struct is the shared {@code Unit}. + * A normal-input op plus a no-input op (input defaults to {@code smithy.api#Unit}), under a + * service with {@code protocolTrait}. Mirrors IAM {@code GetAccountSummary} (shared Unit input). */ private static Model opPlusUnitInputModel(Trait protocolTrait, String normalOp, String unitOp) { StructureShape in = StructureShape.builder().id("com.example#" + normalOp + "Request").build(); From d7522b50553c5a8e15fc3ac6d1d2f900544d0c58 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 15:10:49 -0400 Subject: [PATCH 43/44] Refactor Model transform interface. use streams over loops --- .../generators/model/ModelCodegenPlugin.java | 30 ++++++------ .../generators/model/ModelTransform.java | 25 ++++++++-- .../generators/model/ShapeClassifier.java | 33 ++++++------- .../generators/model/TransformPipeline.java | 4 +- .../model/renderers/SubObjectRenderer.java | 5 -- .../transforms/AccessAnalyzerTransforms.java | 15 +++--- .../transforms/ApiGatewayTransforms.java | 15 +++--- .../transforms/ApiGatewayV2Transforms.java | 15 +++--- .../transforms/ChunkedEncodingTransform.java | 12 ++--- .../model/transforms/DynamoDbTransforms.java | 15 +++--- .../model/transforms/Ec2Transforms.java | 15 +++--- .../model/transforms/GlacierTransforms.java | 15 +++--- .../model/transforms/GlobalTransforms.java | 14 ++++-- .../model/transforms/LambdaTransforms.java | 15 +++--- .../transforms/LongPollingTransform.java | 16 +++---- .../model/transforms/S3ControlTransforms.java | 15 +++--- .../model/transforms/S3Transforms.java | 17 +++---- .../transforms/SourceRegionTransform.java | 15 +++--- .../model/transforms/SqsTransforms.java | 15 +++--- .../SupportsPresigningTransform.java | 12 ++--- .../model/GlobalTransformsTest.java | 34 +++++++------- .../generators/model/ModelGeneratorTest.java | 8 ++-- .../ProtocolTraitsCharacterizationTest.java | 2 +- .../model/SubObjectRendererTest.java | 2 +- .../model/TransformPipelineTest.java | 42 +++++++++++++++-- .../AccessAnalyzerTransformsTest.java | 7 ++- .../transforms/ApiGatewayTransformsTest.java | 5 +- .../ApiGatewayV2TransformsTest.java | 5 +- .../ChunkedEncodingTransformTest.java | 10 ++-- .../transforms/DynamoDbTransformsTest.java | 13 ++---- .../model/transforms/Ec2TransformsTest.java | 15 +++--- .../transforms/GlacierTransformsTest.java | 16 +++---- .../transforms/LambdaTransformsTest.java | 5 +- .../transforms/LongPollingTransformTest.java | 12 ++--- .../transforms/S3ControlTransformsTest.java | 5 +- .../model/transforms/S3TransformsTest.java | 46 +++++++++---------- .../transforms/SourceRegionTransformTest.java | 15 +++--- .../model/transforms/SqsTransformsTest.java | 8 ++-- .../SupportsPresigningTransformTest.java | 10 ++-- 39 files changed, 289 insertions(+), 289 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java index 65905d5e67c..1c9a1a7051b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -53,21 +53,21 @@ public void execute(PluginContext context) { Map namespaceMap = parseNamespaceMap(settings); TransformPipeline pipeline = new TransformPipeline(List.of( - GlobalTransforms.asTransform(), - SourceRegionTransform.asTransform(), - LambdaTransforms.asTransform(), - SqsTransforms.asTransform(), - ApiGatewayTransforms.asTransform(), - ApiGatewayV2Transforms.asTransform(), - Ec2Transforms.asTransform(), - AccessAnalyzerTransforms.asTransform(), - DynamoDbTransforms.asTransform(), - S3Transforms.asTransform(), - S3ControlTransforms.asTransform(), - GlacierTransforms.asTransform(), - SupportsPresigningTransform.asTransform(), - ChunkedEncodingTransform.asTransform(), - LongPollingTransform.asTransform() + new GlobalTransforms(), + new SourceRegionTransform(), + new LambdaTransforms(), + new SqsTransforms(), + new ApiGatewayTransforms(), + new ApiGatewayV2Transforms(), + new Ec2Transforms(), + new AccessAnalyzerTransforms(), + new DynamoDbTransforms(), + new S3Transforms(), + new S3ControlTransforms(), + new GlacierTransforms(), + new SupportsPresigningTransform(), + new ChunkedEncodingTransform(), + new LongPollingTransform() )); CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java index 843e59563d7..105367c4451 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java @@ -10,16 +10,35 @@ /** * A model-to-model transform applied before code generation. Transforms run in sequence, * each receiving the previous transform's output (or the original model for the first). + * + *

{@link #shouldRun} is the service-level gate and defaults to {@code false}: a transform runs + * only when it explicitly opts in by overriding it — a service check, or {@code true} for transforms + * that apply to every service. This fails closed, so a transform added without a gate silently + * no-ops instead of running for every service and mutating models it was never meant to touch. When + * {@code shouldRun} returns true, {@code transform} may assume it applies and need not re-check the + * service. */ -@FunctionalInterface public interface ModelTransform { /** - * Applies this transform to the model. + * Whether this transform applies to the given service. Evaluated by the pipeline before + * {@link #transform}; a false result skips the transform. Defaults to {@code false}, so a + * transform must override this to run — either a service check or {@code true} to run always. + * + * @param service the service shape being generated + * @return true if {@link #transform} should be invoked for this service + */ + default boolean shouldRun(ServiceShape service) { + return false; + } + + /** + * Applies this transform to the model. Only invoked when {@link #shouldRun} returns true, + * so implementations need not re-check the service. * * @param model the current model (may have been modified by earlier transforms) * @param service the service shape being generated * @return the transformed model (may be the same instance if no changes needed) */ - Model apply(Model model, ServiceShape service); + Model transform(Model model, ServiceShape service); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java index 34812a68b20..237f318c3ff 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java @@ -161,21 +161,20 @@ public static ClassifiedShapes classify(Model model, ServiceShape service, Proto } // Shape ids referenced as a member by any reachable shape (includes list/map element targets). - Set memberTargetIds = new HashSet<>(); - for (ShapeId id : reachable) { - model.expectShape(id).members().forEach(m -> memberTargetIds.add(m.getTarget())); - } + Set memberTargetIds = reachable.stream() + .flatMap(id -> model.expectShape(id).members().stream()) + .map(MemberShape::getTarget) + .collect(Collectors.toSet()); // Structs that are members of a reachable @streaming union — i.e. events. A blob-payload // event is recognised only among these, so a plain data struct carrying an @eventPayload // blob is never mis-claimed. - Set eventStructIds = new HashSet<>(); - for (ShapeId id : reachable) { - Shape shape = model.expectShape(id); - if (shape.isUnionShape() && shape.hasTrait(StreamingTrait.class)) { - shape.members().forEach(m -> eventStructIds.add(m.getTarget())); - } - } + Set eventStructIds = reachable.stream() + .map(model::expectShape) + .filter(shape -> shape.isUnionShape() && shape.hasTrait(StreamingTrait.class)) + .flatMap(shape -> shape.members().stream()) + .map(MemberShape::getTarget) + .collect(Collectors.toSet()); // Incoming event-stream union shape ids: the @streaming union member of every event-stream // handler output. Realized via the handler and never referenced as a data type, so their @@ -278,13 +277,11 @@ private static boolean isBlobPayloadEvent(Shape shape, Model model, Set * blob-payload event renderer so they agree on which member becomes the blob payload. */ public static Optional blobPayloadMemberName(StructureShape shape, Model model) { - for (MemberShape member : shape.getAllMembers().values()) { - if (member.hasTrait(EventPayloadTrait.class) - && model.expectShape(member.getTarget()).isBlobShape()) { - return Optional.of(member.getMemberName()); - } - } - return Optional.empty(); + return shape.getAllMembers().values().stream() + .filter(member -> member.hasTrait(EventPayloadTrait.class) + && model.expectShape(member.getTarget()).isBlobShape()) + .map(MemberShape::getMemberName) + .findFirst(); } /** diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java index 4259807bbd1..fcaae7fad13 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java @@ -33,7 +33,9 @@ public TransformPipeline(List transforms) { public Model apply(Model model, ServiceShape service) { Model current = model; for (ModelTransform transform : transforms) { - current = transform.apply(current, service); + if (transform.shouldRun(service)) { + current = transform.transform(current, service); + } } return current; } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java index 477a253f680..8c9d273cc79 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java @@ -33,11 +33,6 @@ public SubObjectRenderer(List subObjects, Set resultOutputIds, R this.ctx = ctx; } - /** Convenience overload for callers with no dual-role output shapes (e.g. characterization tests). */ - public SubObjectRenderer(List subObjects, RenderContext ctx) { - this(subObjects, java.util.Collections.emptySet(), ctx); - } - @Override public void render(CppWriterDelegator writerDelegator) { for (Shape shape : subObjects) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java index d29a00c398c..2be23779b59 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransforms.java @@ -25,18 +25,15 @@ * trait so serde stays correct. Self-guards on service name accessanalyzer; no-op when the domain * shape is absent; throws if GeneratedPolicyResults is already occupied by a distinct shape. */ -public final class AccessAnalyzerTransforms { +public final class AccessAnalyzerTransforms implements ModelTransform { - private AccessAnalyzerTransforms() {} - - public static ModelTransform asTransform() { - return AccessAnalyzerTransforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "accessanalyzer".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"accessanalyzer".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { String ns = service.getId().getNamespace(); ShapeId oldShape = ShapeId.fromParts(ns, "GeneratedPolicyResult"); ShapeId newShape = ShapeId.fromParts(ns, "GeneratedPolicyResults"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java index 5aff1662174..9162529c645 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransforms.java @@ -20,18 +20,15 @@ * Renames the reserved {@code body}/{@code headers} members of API Gateway's test-invoke requests to * {@code requestBody}/{@code requestHeaders}. Mirrors the legacy C2J {@code APIGatewayRestJsonCppClientGenerator}. */ -public final class ApiGatewayTransforms { +public final class ApiGatewayTransforms implements ModelTransform { - private ApiGatewayTransforms() {} - - public static ModelTransform asTransform() { - return ApiGatewayTransforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "api-gateway".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"api-gateway".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { String ns = service.getId().getNamespace(); Protocol protocol = ProtocolResolver.resolve(service, model); List updated = new ArrayList<>(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java index a1636497b07..b6dea0be843 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2Transforms.java @@ -20,18 +20,15 @@ * Renames the reserved {@code Body} member of API Gateway V2's import requests to {@code requestBody}. * Mirrors the legacy C2J {@code APIGatewayV2RestJsonCppClientGenerator}. */ -public final class ApiGatewayV2Transforms { +public final class ApiGatewayV2Transforms implements ModelTransform { - private ApiGatewayV2Transforms() {} - - public static ModelTransform asTransform() { - return ApiGatewayV2Transforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "apigatewayv2".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"apigatewayv2".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { String ns = service.getId().getNamespace(); Protocol protocol = ProtocolResolver.resolve(service, model); List updated = new ArrayList<>(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java index 9f8683c447c..d40bd0e5da0 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransform.java @@ -24,17 +24,17 @@ * hasStreamMembers proxy, also guaranteeing members > 0), and either the service is MediaStore Data * or the operation is S3's WriteGetObjectResponse. No-op otherwise. */ -public final class ChunkedEncodingTransform { +public final class ChunkedEncodingTransform implements ModelTransform { private static final String WRITE_GET_OBJECT_RESPONSE = "WriteGetObjectResponse"; - private ChunkedEncodingTransform() {} - - public static ModelTransform asTransform() { - return ChunkedEncodingTransform::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return true; } - private static Model apply(Model model, ServiceShape service) { + @Override + public Model transform(Model model, ServiceShape service) { boolean mediaStoreData = "mediastore-data".equals(ServiceNameUtil.getSmithyServiceName(service, null)); List updated = new ArrayList<>(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java index 6b7c56ffc45..d2793ad40a2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransforms.java @@ -21,18 +21,15 @@ * shape stays in the model so member references still resolve. Self-guards on service name dynamodb; * no-op when AttributeValue is absent. */ -public final class DynamoDbTransforms { +public final class DynamoDbTransforms implements ModelTransform { - private DynamoDbTransforms() {} - - public static ModelTransform asTransform() { - return DynamoDbTransforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "dynamodb".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"dynamodb".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { ShapeId attributeValueId = ShapeId.fromParts(service.getId().getNamespace(), "AttributeValue"); Optional attributeValue = model.getShape(attributeValueId); if (attributeValue.isEmpty()) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java index dc43824d1cf..e225608c11b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2Transforms.java @@ -28,18 +28,15 @@ * BlobAttributeValue). Out of scope (left to C2J): legacy error-code injection, CopySnapshot * pre-signing, and the endpoint template. */ -public final class Ec2Transforms { +public final class Ec2Transforms implements ModelTransform { - private Ec2Transforms() {} - - public static ModelTransform asTransform() { - return Ec2Transforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "ec2".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"ec2".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { return renameResultShapesToResponse( addSecureBlobUserData(addSpotInstanceStateDisabled(model))); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java index 58aa21d8987..4675df14d8e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransforms.java @@ -33,20 +33,17 @@ * {@link AdditionalRequestHeadersTrait} on their inputs so request rendering emits the matching * headers.insert(...). Self-guards on service name; no-op when the model has no streaming request. */ -public final class GlacierTransforms { +public final class GlacierTransforms implements ModelTransform { private static final String GLACIER_VERSION_HEADER = "x-amz-glacier-version"; - private GlacierTransforms() {} - - public static ModelTransform asTransform() { - return GlacierTransforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "glacier".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"glacier".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { return retypeLimitQueryMembersToString(addAdditionalHeaders(model, service), service); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java index 1823039213f..99040d3eab7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlobalTransforms.java @@ -36,7 +36,7 @@ * Global model transforms applied before code generation. * Handles reserved member renaming and reachability filtering. */ -public final class GlobalTransforms { +public final class GlobalTransforms implements ModelTransform { /** * Services (raw smithy names) that skip the "body" -> "requestBody" rename. api-gateway/apigatewayv2 @@ -61,8 +61,6 @@ public final class GlobalTransforms { */ public static final String RESPONSE_METADATA = "ResponseMetadata"; - private GlobalTransforms() {} - /** * Renames reserved request members on every operation-input structure ({@code body -> requestBody}, * {@code headers/Headers -> headerValues}), honoring the per-service skip-lists. Mirrors C2J's @@ -170,8 +168,14 @@ public static List nonDeprecatedOperations(Model model, ServiceS * sees the pruned model), then {@link #renameReservedRequestMembers}, then * {@link #injectResponseMetadata}. */ - public static ModelTransform asTransform() { - return (model, service) -> injectResponseMetadata( + @Override + public boolean shouldRun(ServiceShape service) { + return true; + } + + @Override + public Model transform(Model model, ServiceShape service) { + return injectResponseMetadata( renameReservedRequestMembers(dropDeprecatedMembers(model, service), service), service); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java index 344442d7543..3da6a97ee65 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransforms.java @@ -23,20 +23,17 @@ * Lambda service. Mirrors the legacy C2J {@code LambdaRestJsonCppClientGenerator}, which removed * {@code InvokeAsync} because it collides with the generated async client. */ -public final class LambdaTransforms { +public final class LambdaTransforms implements ModelTransform { private static final ShapeId UNIT = ShapeId.from("smithy.api#Unit"); - private LambdaTransforms() {} - - public static ModelTransform asTransform() { - return LambdaTransforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "lambda".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"lambda".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { Optional invokeAsync = TopDownIndex.of(model) .getContainedOperations(service).stream() .filter(op -> "InvokeAsync".equals(op.getId().getName())) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java index de10d889b75..fc212625b91 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransform.java @@ -24,7 +24,7 @@ * smithy service name (from {@link ServiceNameUtil#getSmithyServiceName(ServiceShape, Map)} with a * {@code null} map, so no c2jMap remap like {@code sfn->states}). No-op for other services. */ -public final class LongPollingTransform { +public final class LongPollingTransform implements ModelTransform { private static final Map> LONG_POLLING_OPERATIONS = Map.of( "sqs", Set.of("ReceiveMessage"), @@ -32,18 +32,16 @@ public final class LongPollingTransform { "swf", Set.of("PollForActivityTask", "PollForDecisionTask") ); - private LongPollingTransform() {} - - public static ModelTransform asTransform() { - return LongPollingTransform::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return LONG_POLLING_OPERATIONS.containsKey( + ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { + @Override + public Model transform(Model model, ServiceShape service) { Set longPollOps = LONG_POLLING_OPERATIONS.get(ServiceNameUtil.getSmithyServiceName(service, null)); - if (longPollOps == null) { - return model; - } List updated = new ArrayList<>(); for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { if (longPollOps.contains(op.getId().getName())) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java index d510bc260f9..57bcc9ece02 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransforms.java @@ -22,18 +22,15 @@ * each operation-output with {@link TopLevelHostIdTrait}, which {@code ResultRenderer} turns into the * top-level HostId accessor group. Self-guards on the raw smithy service name {@code s3-control}. */ -public final class S3ControlTransforms { +public final class S3ControlTransforms implements ModelTransform { - private S3ControlTransforms() {} - - public static ModelTransform asTransform() { - return S3ControlTransforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "s3-control".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"s3-control".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { TopDownIndex index = TopDownIndex.of(model); List marked = new ArrayList<>(); for (OperationShape op : index.getContainedOperations(service)) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java index 1fa5e3dad1e..ae0793ee0fd 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3Transforms.java @@ -42,19 +42,16 @@ * when its shapes are absent and fast-fails on genuine collisions. Client/endpoint/ARN/S3Express/CRT * customizations and serde-body emission are out of scope. */ -public final class S3Transforms { +public final class S3Transforms implements ModelTransform { - private S3Transforms() {} - - public static ModelTransform asTransform() { - return S3Transforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + String name = ServiceNameUtil.getSmithyServiceName(service, null); + return "s3".equals(name) || "s3-crt".equals(name); } - private static Model apply(Model model, ServiceShape service) { - String name = ServiceNameUtil.getSmithyServiceName(service, null); - if (!"s3".equals(name) && !"s3-crt".equals(name)) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { Model result = markEmbeddedErrors(injectAccessLogTagQuery(normalizeReplicationStatus( expandBucketLocationConstraint(hackGetObjectResult( addExpiresCustomization(renameCopyObjectResult( diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java index b8ec549a219..947dfe257ba 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransform.java @@ -24,7 +24,7 @@ * RDS-family services (RDS/DocDB/Neptune), mirroring the C2J injection that backs presigned-URL * generation. Model-shape scope only; the presigned-URL client logic remains in the C2J path. */ -public final class SourceRegionTransform { +public final class SourceRegionTransform implements ModelTransform { private static final String SOURCE_REGION = "SourceRegion"; @@ -44,18 +44,15 @@ public final class SourceRegionTransform { "CreateDBCluster") ); - private SourceRegionTransform() {} - - public static ModelTransform asTransform() { - return SourceRegionTransform::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return TARGETS.containsKey(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { + @Override + public Model transform(Model model, ServiceShape service) { String serviceName = ServiceNameUtil.getSmithyServiceName(service, null); Set operations = TARGETS.get(serviceName); - if (operations == null) { - return model; - } List updated = new ArrayList<>(); for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java index 4c6698b8f6c..32c69f2ed59 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransforms.java @@ -16,22 +16,19 @@ * {@code SQSQueryXmlCppClientGenerator}/{@code SQSJsonCppClientGenerator} injected. These values are * returned by the service but absent from the model. */ -public final class SqsTransforms { +public final class SqsTransforms implements ModelTransform { private static final String ENUM_NAME = "QueueAttributeName"; private static final List ADDED_VALUES = List.of( "SentTimestamp", "ApproximateFirstReceiveTimestamp", "ApproximateReceiveCount", "SenderId"); - private SqsTransforms() {} - - public static ModelTransform asTransform() { - return SqsTransforms::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return "sqs".equals(ServiceNameUtil.getSmithyServiceName(service, null)); } - private static Model apply(Model model, ServiceShape service) { - if (!"sqs".equals(ServiceNameUtil.getSmithyServiceName(service, null))) { - return model; - } + @Override + public Model transform(Model model, ServiceShape service) { return TransformSupport.appendEnumValuesByName(model, ENUM_NAME, ADDED_VALUES); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java index 3eb51cfce34..78c8337147d 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransform.java @@ -24,15 +24,15 @@ * also covers Unit-input operations. C2J sets the flag for every query/ec2 operation, plus Polly's * {@code SynthesizeSpeech}. No-op for other services. */ -public final class SupportsPresigningTransform { +public final class SupportsPresigningTransform implements ModelTransform { - private SupportsPresigningTransform() {} - - public static ModelTransform asTransform() { - return SupportsPresigningTransform::apply; + @Override + public boolean shouldRun(ServiceShape service) { + return true; } - private static Model apply(Model model, ServiceShape service) { + @Override + public Model transform(Model model, ServiceShape service) { Protocol protocol = ProtocolResolver.resolve(service, model); boolean queryLike = protocol == Protocol.QUERY_XML || protocol == Protocol.EC2; boolean polly = "polly".equals(ServiceNameUtil.getSmithyServiceName(service, null)); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index 34c2568221d..80868cd9e88 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java @@ -42,7 +42,7 @@ private static StructureShape input(Model m) { @Test void reservedRename_body_becomesRequestBody_forNonSkippedService() { Model m = inputModel("Security IR", "body"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); assertFalse(input(out).getMember("body").isPresent()); assertTrue(input(out).getMember("requestBody").isPresent()); } @@ -50,7 +50,7 @@ void reservedRename_body_becomesRequestBody_forNonSkippedService() { @Test void reservedRename_body_skippedForBedrockRuntime() { Model m = inputModel("Bedrock Runtime", "body"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); assertTrue(input(out).getMember("body").isPresent(), "skip-listed service keeps body"); } @@ -59,14 +59,14 @@ void reservedRename_body_skippedForApiGateway_rawName() { // C2J name is "apigateway" but the raw smithy name is "api-gateway"; the skip-list must use // the raw name or API Gateway's dedicated transform gets pre-empted. Model m = inputModel("API Gateway", "body"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); assertTrue(input(out).getMember("body").isPresent(), "api-gateway must be skipped"); } @Test void reservedRename_headers_becomesHeaderValues_forNonSkippedService() { Model m = inputModel("Kinesis", "headers"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); assertTrue(input(out).getMember("headerValues").isPresent()); assertFalse(input(out).getMember("headers").isPresent()); } @@ -74,7 +74,7 @@ void reservedRename_headers_becomesHeaderValues_forNonSkippedService() { @Test void reservedRename_capitalHeaders_alwaysRenamed() { Model m = inputModel("API Gateway", "Headers"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); assertTrue(input(out).getMember("headerValues").isPresent()); } @@ -94,7 +94,7 @@ void reservedRename_onlyTouchesOperationInputs_notArbitraryShapes() { .sdkId("Kinesis").arnNamespace("x").cloudFormationName("X").cloudTrailEventSource("x").build()) .addOperation(op.getId()).build(); Model m = Model.assembler().addShapes(domain, input, output, op, service).assemble().unwrap(); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); assertTrue(out.expectShape(ShapeId.from("com.example#HttpThing"), StructureShape.class) .getMember("body").isPresent(), "domain shape body must not be renamed"); } @@ -103,13 +103,13 @@ void reservedRename_onlyTouchesOperationInputs_notArbitraryShapes() { void reservedRename_collision_throws() { Model m = inputModel("Kinesis", "body", "requestBody"); assertThrows(IllegalStateException.class, - () -> GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example"))); + () -> new GlobalTransforms().transform(m, serviceOf(m, "Example"))); } @Test void reservedRename_jsonService_preservesWireNameWithJsonName() { Model m = inputModel("Kinesis", "body"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); MemberShape renamed = input(out).getMember("requestBody").orElseThrow(); assertEquals("body", renamed.expectTrait(JsonNameTrait.class).getValue(), "JSON service must keep the 'body' wire key via @jsonName"); @@ -120,7 +120,7 @@ void reservedRename_jsonService_preservesWireNameWithJsonName() { void reservedRename_queryXmlService_preservesWireNameWithXmlName() { Model m = inputModelWithProtocol("Kinesis", new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait(), "body"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); MemberShape renamed = input(out).getMember("requestBody").orElseThrow(); assertEquals("body", renamed.expectTrait(XmlNameTrait.class).getValue(), "awsQuery service must keep the 'body' wire key via @xmlName"); @@ -136,7 +136,7 @@ void reservedRename_ec2Service_pinsRequestKeyAndResponseName() { // name (@xmlName) differ, so both are pinned rather than relying on capitalize(@xmlName). Model m = inputModelWithProtocol("Kinesis", new software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait(), "body"); - Model out = GlobalTransforms.asTransform().apply(m, serviceOf(m, "Example")); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); MemberShape renamed = input(out).getMember("requestBody").orElseThrow(); assertEquals("Body", renamed.expectTrait( software.amazon.smithy.aws.traits.protocols.Ec2QueryNameTrait.class).getValue(), @@ -699,7 +699,7 @@ private static ServiceShape serviceOf(Model model) { @Test void injectResponseMetadata_awsQuery_addsResponseMetadataMemberToResult() { Model model = oneOutputModel(new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait()); - Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + Model out = new GlobalTransforms().transform(model, serviceOf(model)); StructureShape result = out.expectShape( ShapeId.from("com.example#DoThingOutput"), StructureShape.class); @@ -713,7 +713,7 @@ void injectResponseMetadata_awsQuery_addsResponseMetadataMemberToResult() { @Test void injectResponseMetadata_awsQuery_addsResponseMetadataStructureWithRequestId() { Model model = oneOutputModel(new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait()); - Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + Model out = new GlobalTransforms().transform(model, serviceOf(model)); ShapeId rmId = out.expectShape(ShapeId.from("com.example#DoThingOutput"), StructureShape.class) .getMember("ResponseMetadata").get().getTarget(); @@ -726,7 +726,7 @@ void injectResponseMetadata_awsQuery_addsResponseMetadataStructureWithRequestId( @Test void injectResponseMetadata_ec2_addsResponseMetadataMemberToResult() { Model model = oneOutputModel(new software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait()); - Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + Model out = new GlobalTransforms().transform(model, serviceOf(model)); StructureShape result = out.expectShape( ShapeId.from("com.example#DoThingOutput"), StructureShape.class); @@ -738,7 +738,7 @@ void injectResponseMetadata_ec2_addsResponseMetadataMemberToResult() { void injectResponseMetadata_restJson_leavesResultUnchanged() { Model model = oneOutputModel( software.amazon.smithy.aws.traits.protocols.RestJson1Trait.builder().build()); - Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + Model out = new GlobalTransforms().transform(model, serviceOf(model)); StructureShape result = out.expectShape( ShapeId.from("com.example#DoThingOutput"), StructureShape.class); @@ -753,7 +753,7 @@ void injectResponseMetadata_awsJsonWithQueryCompatible_addsResponseMetadataMembe Model model = oneOutputModel( software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait.builder().build(), new software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait()); - Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + Model out = new GlobalTransforms().transform(model, serviceOf(model)); StructureShape result = out.expectShape( ShapeId.from("com.example#DoThingOutput"), StructureShape.class); @@ -769,7 +769,7 @@ void injectResponseMetadata_awsJsonWithQueryCompatible_addsResponseMetadataStruc Model model = oneOutputModel( software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait.builder().build(), new software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait()); - Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + Model out = new GlobalTransforms().transform(model, serviceOf(model)); ShapeId rmId = out.expectShape(ShapeId.from("com.example#DoThingOutput"), StructureShape.class) .getMember("ResponseMetadata").get().getTarget(); @@ -831,7 +831,7 @@ void injectResponseMetadata_awsJsonWithoutQueryCompatible_leavesResultUnchanged( // Plain awsJson1_0 (no @awsQueryCompatible) must NOT get ResponseMetadata injected. Model model = oneOutputModel( software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait.builder().build()); - Model out = GlobalTransforms.asTransform().apply(model, serviceOf(model)); + Model out = new GlobalTransforms().transform(model, serviceOf(model)); StructureShape result = out.expectShape( ShapeId.from("com.example#DoThingOutput"), StructureShape.class); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java index 993a727d05d..2f147eb11c0 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGeneratorTest.java @@ -81,9 +81,11 @@ private static MockManifest generate(String smithyServiceName, String namespace, Model model = model(smithyServiceName); ServiceShape service = model.expectShape( ShapeId.from("com.amazonaws.dynamodb#DynamoDB_20120810"), ServiceShape.class); - // Apply the DynamoDB service-level transform first (mirrors ModelCodegenPlugin): marks - // AttributeValue @customRendered for dynamodb, no-op otherwise. Suppression flows through ShapeClassifier. - Model transformed = DynamoDbTransforms.asTransform().apply(model, service); + // Apply the DynamoDB service-level transform first (mirrors ModelCodegenPlugin): its + // shouldRun gate marks AttributeValue @customRendered for dynamodb only, so any other + // service is skipped and AttributeValue flows through ShapeClassifier generically. + var ddb = new DynamoDbTransforms(); + Model transformed = ddb.shouldRun(service) ? ddb.transform(model, service) : model; MockManifest manifest = new MockManifest(); CppWriterDelegator delegator = new CppWriterDelegator(manifest); new ModelGenerator(transformed, service, delegator, smithyServiceName, exportMacro, namespace) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java index 4709a4bf4a6..727ddcd3482 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolTraitsCharacterizationTest.java @@ -127,7 +127,7 @@ private static java.util.Map renderAll(Protocol p) { ProtocolTraits traits = ProtocolResolver.traitsFor(resolved); RenderContext ctx = new RenderContext(model, service, traits, "Example", "AWS_EXAMPLE_API", "example"); - new SubObjectRenderer(classified.subObjects(), ctx).render(delegator); + new SubObjectRenderer(classified.subObjects(), classified.resultOutputIds(), ctx).render(delegator); new RequestRenderer(classified.requests(), ctx).render(delegator); new ResultRenderer(classified.results(), ctx).render(delegator); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java index 01bfb42a133..891480b256b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SubObjectRendererTest.java @@ -85,7 +85,7 @@ private static java.util.Map renderAll() { model.expectShape(ShapeId.from("com.example#BidirectionalInput")), model.expectShape(ShapeId.from("com.example#AnyToolChoice")), model.expectShape(ShapeId.from("com.example#AudioSource"))); - new SubObjectRenderer(subObjects, + new SubObjectRenderer(subObjects, java.util.Collections.emptySet(), new RenderContext(model, service, traits, "Example", "AWS_EXAMPLE_API", "example")).render(delegator); delegator.flushWriters(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java index 04506885b12..3e006c7ea4e 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java @@ -11,12 +11,27 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.BiFunction; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; class TransformPipelineTest { + /** Wraps a transform body as an always-run transform (shouldRun defaults to false otherwise). */ + private static ModelTransform alwaysRun(BiFunction body) { + return new ModelTransform() { + @Override + public boolean shouldRun(ServiceShape service) { + return true; + } + @Override + public Model transform(Model model, ServiceShape service) { + return body.apply(model, service); + } + }; + } + @Test void emptyPipelineReturnsModelUnchanged() { Model model = Model.builder() @@ -38,8 +53,8 @@ void transformsExecuteInOrder() { ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); List executionOrder = new ArrayList<>(); - ModelTransform first = (m, s) -> { executionOrder.add("first"); return m; }; - ModelTransform second = (m, s) -> { executionOrder.add("second"); return m; }; + ModelTransform first = alwaysRun((m, s) -> { executionOrder.add("first"); return m; }); + ModelTransform second = alwaysRun((m, s) -> { executionOrder.add("second"); return m; }); TransformPipeline pipeline = new TransformPipeline(List.of(first, second)); pipeline.apply(model, service); @@ -58,12 +73,12 @@ void transformReceivesOutputOfPrevious() { .addShape(ServiceShape.builder().id("com.example#Extra").version("2024-01-01").build()) .build(); - ModelTransform addShape = (m, s) -> withExtra; - ModelTransform checkShape = (m, s) -> { + ModelTransform addShape = alwaysRun((m, s) -> withExtra); + ModelTransform checkShape = alwaysRun((m, s) -> { // This transform should see the shape added by the first m.expectShape(ShapeId.from("com.example#Extra")); return m; - }; + }); TransformPipeline pipeline = new TransformPipeline(List.of(addShape, checkShape)); Model result = pipeline.apply(original, service); @@ -71,4 +86,21 @@ void transformReceivesOutputOfPrevious() { // Final result is the model from the last transform assertSame(withExtra, result); } + + @Test + void skipsTransformThatDoesNotOptIn() { + Model model = Model.builder() + .addShape(ServiceShape.builder().id("com.example#TestService").version("2024-01-01").build()) + .build(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); + + List executionOrder = new ArrayList<>(); + // A bare transform inherits shouldRun == false, so the pipeline must skip it. + ModelTransform notOptedIn = (m, s) -> { executionOrder.add("skipped"); return m; }; + ModelTransform optedIn = alwaysRun((m, s) -> { executionOrder.add("ran"); return m; }); + + new TransformPipeline(List.of(notOptedIn, optedIn)).apply(model, service); + + assertEquals(List.of("ran"), executionOrder); + } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java index 8ad9cda0120..2b5e91b311a 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/AccessAnalyzerTransformsTest.java @@ -40,7 +40,7 @@ private static ServiceShape service(Model m) { @Test void renamesShapeAndMember_withJsonNamePreserved() { Model m = model("AccessAnalyzer", true); - Model out = AccessAnalyzerTransforms.asTransform().apply(m, service(m)); + Model out = new AccessAnalyzerTransforms().transform(m, service(m)); assertTrue(out.getShape( ShapeId.from("com.amazonaws.accessanalyzer#GeneratedPolicyResults")).isPresent(), @@ -62,8 +62,7 @@ void renamesShapeAndMember_withJsonNamePreserved() { @Test void noOpForOtherService() { Model m = model("SomethingElse", true); - Model out = AccessAnalyzerTransforms.asTransform().apply(m, service(m)); - assertSame(m, out); + assertFalse(new AccessAnalyzerTransforms().shouldRun(service(m))); } @Test @@ -84,6 +83,6 @@ void throwsWhenTargetShapeAlreadyExists() { .addOperation(op.getId()).build(); Model m = Model.assembler().addShapes(gpr, gprs, resp, req, op, svc).assemble().unwrap(); assertThrows(IllegalStateException.class, - () -> AccessAnalyzerTransforms.asTransform().apply(m, service(m))); + () -> new AccessAnalyzerTransforms().transform(m, service(m))); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java index cc70ad9c1c6..42e4382fad0 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayTransformsTest.java @@ -52,7 +52,7 @@ private static ServiceShape service(Model m) { @Test void renamesBodyAndHeaders() { Model m = apiGatewayModel("API Gateway"); - Model out = ApiGatewayTransforms.asTransform().apply(m, service(m)); + Model out = new ApiGatewayTransforms().transform(m, service(m)); StructureShape r = out.expectShape( ShapeId.from("com.example#TestInvokeMethodRequest"), StructureShape.class); @@ -74,7 +74,6 @@ void renamesBodyAndHeaders() { @Test void noOpForOtherService() { Model m = apiGatewayModel("SomeOther"); - Model out = ApiGatewayTransforms.asTransform().apply(m, service(m)); - assertSame(m, out); + assertFalse(new ApiGatewayTransforms().shouldRun(service(m))); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java index 9ac34b20a9e..21a8c62aac9 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ApiGatewayV2TransformsTest.java @@ -47,7 +47,7 @@ private static ServiceShape service(Model m) { @Test void renamesBody() { Model m = model("ApiGatewayV2"); - Model out = ApiGatewayV2Transforms.asTransform().apply(m, service(m)); + Model out = new ApiGatewayV2Transforms().transform(m, service(m)); for (String name : new String[]{"ImportApiRequest", "ReimportApiRequest"}) { StructureShape r = out.expectShape(ShapeId.from("com.example#" + name), StructureShape.class); assertTrue(r.getMember("requestBody").isPresent(), name); @@ -60,7 +60,6 @@ void renamesBody() { @Test void noOpForOtherService() { Model m = model("SomeOther"); - Model out = ApiGatewayV2Transforms.asTransform().apply(m, service(m)); - assertSame(m, out); + assertFalse(new ApiGatewayV2Transforms().shouldRun(service(m))); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java index bc3515b8060..035f70ca240 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/ChunkedEncodingTransformTest.java @@ -68,7 +68,7 @@ private static Model oneOpModel(String sdkId, String opName, boolean streaming, @Test void mediaStoreDataUnsignedStreamingOp_stampsInput() { Model m = oneOpModel("MediaStore Data", "PutObject", true, true); - Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + Model out = new ChunkedEncodingTransform().transform(m, service(m)); assertTrue(stamped(out, "PutObjectRequest"), "MediaStore Data unsigned-payload streaming request must be stamped"); } @@ -76,7 +76,7 @@ void mediaStoreDataUnsignedStreamingOp_stampsInput() { @Test void mediaStoreDataNonStreamingOp_notStamped() { Model m = oneOpModel("MediaStore Data", "DescribeObject", false, true); - Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + Model out = new ChunkedEncodingTransform().transform(m, service(m)); assertSame(m, out, "no qualifying operation must leave the model untouched"); assertFalse(stamped(out, "DescribeObjectRequest"), "a non-streaming request must not be stamped"); @@ -85,7 +85,7 @@ void mediaStoreDataNonStreamingOp_notStamped() { @Test void mediaStoreDataSignedStreamingOp_notStamped() { Model m = oneOpModel("MediaStore Data", "PutObject", true, false); - Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + Model out = new ChunkedEncodingTransform().transform(m, service(m)); assertSame(m, out, "no qualifying operation must leave the model untouched"); assertFalse(stamped(out, "PutObjectRequest"), "a signed (no @unsignedPayload) request must not be stamped"); @@ -94,7 +94,7 @@ void mediaStoreDataSignedStreamingOp_notStamped() { @Test void s3WriteGetObjectResponseUnsignedStreamingOp_stampsInput() { Model m = oneOpModel("S3", "WriteGetObjectResponse", true, true); - Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + Model out = new ChunkedEncodingTransform().transform(m, service(m)); assertTrue(stamped(out, "WriteGetObjectResponseRequest"), "S3 WriteGetObjectResponse unsigned-payload streaming request must be stamped"); } @@ -102,7 +102,7 @@ void s3WriteGetObjectResponseUnsignedStreamingOp_stampsInput() { @Test void unrelatedServiceStreamingOp_notStamped() { Model m = oneOpModel("S3", "PutObject", true, true); - Model out = ChunkedEncodingTransform.asTransform().apply(m, service(m)); + Model out = new ChunkedEncodingTransform().transform(m, service(m)); assertSame(m, out, "an unrelated operation must leave the model untouched"); assertFalse(stamped(out, "PutObjectRequest"), "only WriteGetObjectResponse (or MediaStore Data) may be stamped"); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java index 225c674c4a7..4d73c507999 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/DynamoDbTransformsTest.java @@ -60,7 +60,7 @@ private static ServiceShape service(Model m) { @Test void marksAttributeValueForDynamoDb() { Model m = model("DynamoDB", true); - Model out = DynamoDbTransforms.asTransform().apply(m, service(m)); + Model out = new DynamoDbTransforms().transform(m, service(m)); assertTrue(out.expectShape(ShapeId.from(NS + "#AttributeValue")) .hasTrait(CustomRenderedTrait.class), @@ -69,19 +69,16 @@ void marksAttributeValueForDynamoDb() { @Test void noOpForOtherService() { - // A non-dynamodb service (sdkId resolves via getSmithyServiceName) is untouched. + // A non-dynamodb service (sdkId resolves via getSmithyServiceName) does not run. Model m = model("Kinesis", true); - Model out = DynamoDbTransforms.asTransform().apply(m, service(m)); - assertSame(m, out, "transform must be a no-op for non-dynamodb services"); - assertFalse(out.expectShape(ShapeId.from(NS + "#AttributeValue")) - .hasTrait(CustomRenderedTrait.class), - "non-dynamodb AttributeValue must not be marked"); + assertFalse(new DynamoDbTransforms().shouldRun(service(m)), + "transform must not run for non-dynamodb services"); } @Test void noOpWhenAttributeValueAbsent() { Model m = model("DynamoDB", false); - Model out = DynamoDbTransforms.asTransform().apply(m, service(m)); + Model out = new DynamoDbTransforms().transform(m, service(m)); assertSame(m, out, "transform must be a no-op when AttributeValue is absent"); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java index 442a7188edf..e3baad7b016 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/Ec2TransformsTest.java @@ -46,7 +46,7 @@ private static ServiceShape ec2Service(String sdkId) { @Test void addsDisabledToSpotInstanceState() { Model m = ec2Model("EC2"); - Model out = Ec2Transforms.asTransform().apply(m, service(m)); + Model out = new Ec2Transforms().transform(m, service(m)); assertTrue(EnumRenderer.getEnumValues( out.expectShape(ShapeId.from("com.example#SpotInstanceState"))).contains("disabled")); } @@ -54,8 +54,7 @@ void addsDisabledToSpotInstanceState() { @Test void noOpForOtherService() { Model m = ec2Model("SomeOther"); - Model out = Ec2Transforms.asTransform().apply(m, service(m)); - assertSame(m, out); + assertFalse(new Ec2Transforms().shouldRun(service(m))); } @Test @@ -65,7 +64,7 @@ void renamesNestedResultStructToResponse() { ServiceShape service = ec2Service("EC2"); Model m = Model.assembler().addShapes(nested, service).assemble().unwrap(); - Model out = Ec2Transforms.asTransform().apply(m, service); + Model out = new Ec2Transforms().transform(m, service); assertFalse(out.getShape(ShapeId.from("com.example#MetricDataResult")).isPresent()); assertTrue(out.getShape(ShapeId.from("com.example#MetricDataResponse")).isPresent()); @@ -88,7 +87,7 @@ void throwsWhenResponseShapeAlreadyExists() { .addOperation(op.getId()).build(); Model m = Model.assembler().addShapes(result, response, in, out, op, service).assemble().unwrap(); assertThrows(IllegalStateException.class, - () -> Ec2Transforms.asTransform().apply(m, service(m))); + () -> new Ec2Transforms().transform(m, service(m))); } /** @@ -122,7 +121,7 @@ private static Model userDataModel() { @Test void modelsUserDataAsSensitiveSecureBlobAttributeValue() { Model m = userDataModel(); - Model out = Ec2Transforms.asTransform().apply(m, service(m)); + Model out = new Ec2Transforms().transform(m, service(m)); // SecureBlobAttributeValue exists with a Value member targeting a @sensitive blob // (a @sensitive blob maps to Aws::Utils::CryptoBuffer, matching the C2J baseline). @@ -151,8 +150,8 @@ void modelsUserDataAsSensitiveSecureBlobAttributeValue() { void throwsWhenSecureBlobAttributeValueAlreadyExists() { // Once upstream aws-models adds SecureBlobAttributeValue, this compensating transform is // obsolete. Fail loudly so a human removes it, rather than silently self-retiring. - Model once = Ec2Transforms.asTransform().apply(userDataModel(), service(userDataModel())); + Model once = new Ec2Transforms().transform(userDataModel(), service(userDataModel())); assertThrows(IllegalStateException.class, - () -> Ec2Transforms.asTransform().apply(once, service(once))); + () -> new Ec2Transforms().transform(once, service(once))); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java index 2aba1d64a30..f85f64e9768 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/GlacierTransformsTest.java @@ -85,9 +85,7 @@ void noOpForOtherService() { ServiceShape svc = ServiceShape.builder().id("com.amazonaws.other#Other").version("1") .addTrait(ServiceTrait.builder().sdkId("Other").arnNamespace("other") .cloudFormationName("Other").cloudTrailEventSource("other").build()).build(); - Model m = Model.builder().addShape(svc).build(); - Model out = GlacierTransforms.asTransform().apply(m, svc); - assertSame(m, out, "non-glacier service must be untouched"); + assertFalse(new GlacierTransforms().shouldRun(svc), "non-glacier service must not run"); } @Test @@ -95,7 +93,7 @@ void stampsVersionHeaderOnStreamingRequestInputs() { StructureShape upload = streamingInput("UploadArchiveInput"); OperationShape uploadOp = op("UploadArchive", upload); ServiceShape svc = glacierService("Glacier", uploadOp); - Model out = GlacierTransforms.asTransform().apply(modelWith(svc, upload, uploadOp), svc); + Model out = new GlacierTransforms().transform(modelWith(svc, upload, uploadOp), svc); AdditionalRequestHeadersTrait trait = out .expectShape(ShapeId.from(NS + "#UploadArchiveInput"), StructureShape.class) @@ -111,7 +109,7 @@ void doesNotStampNonStreamingRequestInputs() { StructureShape plain = plainInput("CompleteVaultLockInput"); OperationShape plainOp = op("CompleteVaultLock", plain); ServiceShape svc = glacierService("Glacier", plainOp); - Model out = GlacierTransforms.asTransform().apply(modelWith(svc, plain, plainOp), svc); + Model out = new GlacierTransforms().transform(modelWith(svc, plain, plainOp), svc); assertFalse(out.expectShape(ShapeId.from(NS + "#CompleteVaultLockInput"), StructureShape.class) .hasTrait(AdditionalRequestHeadersTrait.class), @@ -123,7 +121,7 @@ void retypesQueryLimitMemberBackToString() { StructureShape listJobs = queryLimitInput("ListJobsInput"); OperationShape listJobsOp = op("ListJobs", listJobs); ServiceShape svc = glacierService("Glacier", listJobsOp); - Model out = GlacierTransforms.asTransform().apply(modelWith(svc, listJobs, listJobsOp), svc); + Model out = new GlacierTransforms().transform(modelWith(svc, listJobs, listJobsOp), svc); MemberShape limit = out.expectShape(ShapeId.from(NS + "#ListJobsInput"), StructureShape.class) .getMember("limit").orElseThrow(); @@ -140,7 +138,7 @@ void leavesQueryLimitUnchangedWhenAlreadyString() { .build(); OperationShape listJobsOp = op("ListJobs", listJobs); ServiceShape svc = glacierService("Glacier", listJobsOp); - Model out = GlacierTransforms.asTransform().apply(modelWith(svc, listJobs, listJobsOp), svc); + Model out = new GlacierTransforms().transform(modelWith(svc, listJobs, listJobsOp), svc); MemberShape limit = out.expectShape(ShapeId.from(NS + "#ListJobsInput"), StructureShape.class) .getMember("limit").orElseThrow(); @@ -152,8 +150,8 @@ void isIdempotent() { StructureShape upload = streamingInput("UploadArchiveInput"); OperationShape uploadOp = op("UploadArchive", upload); ServiceShape svc = glacierService("Glacier", uploadOp); - Model once = GlacierTransforms.asTransform().apply(modelWith(svc, upload, uploadOp), svc); - Model twice = GlacierTransforms.asTransform().apply(once, svc); + Model once = new GlacierTransforms().transform(modelWith(svc, upload, uploadOp), svc); + Model twice = new GlacierTransforms().transform(once, svc); assertTrue(twice.expectShape(ShapeId.from(NS + "#UploadArchiveInput"), StructureShape.class) .hasTrait(AdditionalRequestHeadersTrait.class), diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java index def0b08b431..81034fc2313 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LambdaTransformsTest.java @@ -42,7 +42,7 @@ private static ServiceShape service(Model m) { @Test void removesInvokeAsyncOperationAndShapes() { Model m = lambdaModel("Lambda"); - Model out = LambdaTransforms.asTransform().apply(m, service(m)); + Model out = new LambdaTransforms().transform(m, service(m)); assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsync")).isEmpty()); assertTrue(out.getShape(ShapeId.from("com.example#InvokeAsyncRequest")).isEmpty()); @@ -54,7 +54,6 @@ void removesInvokeAsyncOperationAndShapes() { @Test void noOpForOtherService() { Model m = lambdaModel("SomeOther"); - Model out = LambdaTransforms.asTransform().apply(m, service(m)); - assertSame(m, out); + assertFalse(new LambdaTransforms().shouldRun(service(m))); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java index 6a96b92c588..aba0216fbf1 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/LongPollingTransformTest.java @@ -62,7 +62,7 @@ private static Model model(String sdkId, String... opNames) { @Test void sqsReceiveMessage_stampsOnlyReceiveMessageInput() { Model m = model("SQS", "ReceiveMessage", "SendMessage"); - Model out = LongPollingTransform.asTransform().apply(m, service(m)); + Model out = new LongPollingTransform().transform(m, service(m)); assertTrue(stamped(out, "ReceiveMessageRequest"), "SQS ReceiveMessage input must be stamped"); assertFalse(stamped(out, "SendMessageRequest"), @@ -72,7 +72,7 @@ void sqsReceiveMessage_stampsOnlyReceiveMessageInput() { @Test void swf_stampsBothPollOperations() { Model m = model("SWF", "PollForActivityTask", "PollForDecisionTask", "StartWorkflowExecution"); - Model out = LongPollingTransform.asTransform().apply(m, service(m)); + Model out = new LongPollingTransform().transform(m, service(m)); assertTrue(stamped(out, "PollForActivityTaskRequest"), "SWF PollForActivityTask input must be stamped"); assertTrue(stamped(out, "PollForDecisionTaskRequest"), @@ -84,7 +84,7 @@ void swf_stampsBothPollOperations() { @Test void sfnGetActivityTask_stampsInput() { Model m = model("SFN", "GetActivityTask", "StartExecution"); - Model out = LongPollingTransform.asTransform().apply(m, service(m)); + Model out = new LongPollingTransform().transform(m, service(m)); assertTrue(stamped(out, "GetActivityTaskRequest"), "SFN GetActivityTask input must be stamped"); assertFalse(stamped(out, "StartExecutionRequest"), @@ -94,9 +94,7 @@ void sfnGetActivityTask_stampsInput() { @Test void unrelatedService_stampsNothing() { Model m = model("DynamoDB", "GetItem", "ReceiveMessage"); - Model out = LongPollingTransform.asTransform().apply(m, service(m)); - assertSame(m, out, "an unrelated service must leave the model untouched"); - assertFalse(stamped(out, "ReceiveMessageRequest"), - "an operation on an unrelated service must not be stamped even if its name matches"); + assertFalse(new LongPollingTransform().shouldRun(service(m)), + "an unrelated service must not run even if an operation name matches"); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java index b8b2e9f35d1..b5678579cee 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3ControlTransformsTest.java @@ -34,7 +34,7 @@ static ServiceShape service(Model m) { @Test void marksResultShapesWithHostIdTrait() { Model m = model("S3 Control"); - Model out = S3ControlTransforms.asTransform().apply(m, service(m)); + Model out = new S3ControlTransforms().transform(m, service(m)); assertTrue(out.expectShape(ShapeId.from(NS + "#CreateAccessPointResult")) .hasTrait(TopLevelHostIdTrait.class), "result shape marked"); } @@ -42,7 +42,6 @@ void marksResultShapesWithHostIdTrait() { @Test void noOpForOtherService() { Model m = model("SomethingElse"); - Model out = S3ControlTransforms.asTransform().apply(m, service(m)); - assertSame(m, out); + assertFalse(new S3ControlTransforms().shouldRun(service(m))); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java index 89e52711383..b4d560f86ec 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/S3TransformsTest.java @@ -43,9 +43,7 @@ void noOpForOtherService() { ServiceShape svc = ServiceShape.builder().id("com.amazonaws.other#Other").version("1") .addTrait(ServiceTrait.builder().sdkId("Other").arnNamespace("other") .cloudFormationName("Other").cloudTrailEventSource("other").build()).build(); - Model m = Model.builder().addShape(svc).build(); - Model out = S3Transforms.asTransform().apply(m, svc); - assertSame(m, out, "non-s3 service must be untouched"); + assertFalse(new S3Transforms().shouldRun(svc), "non-s3 service must not run"); } @Test @@ -53,7 +51,7 @@ void noOpForS3WhenNothingToDo() { ServiceShape svc = s3Service("S3"); Model m = modelWith(svc); // Scaffold has no sub-transforms yet: s3 model returns unchanged (structurally equal). - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); assertNotNull(out); assertTrue(out.getShape(ShapeId.from(NS + "#AmazonS3")).isPresent()); } @@ -74,7 +72,7 @@ void renamesCopyObjectResultShapeAndMember() { StructureShape copyOutput = StructureShape.builder().id(NS + "#CopyObjectOutput") .addMember("CopyObjectResult", copyResult.getId()).build(); Model m = modelWith(svc, copyResult, copyOutput); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); assertTrue(out.getShape(ShapeId.from(NS + "#CopyObjectResultDetails")).isPresent(), "shape renamed to CopyObjectResultDetails"); @@ -100,7 +98,7 @@ void copyObjectResultRename_throwsOnCollision() { StructureShape details = StructureShape.builder().id(NS + "#CopyObjectResultDetails") .addMember("Other", ShapeId.from("smithy.api#String")).build(); Model m = modelWith(svc, copyResult, details); - assertThrows(IllegalStateException.class, () -> S3Transforms.asTransform().apply(m, svc)); + assertThrows(IllegalStateException.class, () -> new S3Transforms().transform(m, svc)); } /** PutObject-style op whose input and output both carry a {@code string} {@code Expires} member. */ @@ -134,7 +132,7 @@ void retypesExpiresShapeToTimestamp() { Model m = expiresModel(); assertTrue(m.expectShape(ShapeId.from(NS + "#Expires")).isStringShape(), "precondition: Expires starts as a string"); - Model out = S3Transforms.asTransform().apply(m, expiresService(m)); + Model out = new S3Transforms().transform(m, expiresService(m)); assertTrue(out.expectShape(ShapeId.from(NS + "#Expires")) instanceof TimestampShape, "Expires retyped to a timestamp shape"); StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); @@ -172,7 +170,7 @@ void retypesPartNumberMarkersToInteger() { assertTrue(m.expectShape(ShapeId.from(NS + "#NextPartNumberMarker")).isStringShape(), "precondition: NextPartNumberMarker starts as a string"); ServiceShape svc = m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); assertTrue(out.expectShape(ShapeId.from(NS + "#PartNumberMarker")) instanceof IntegerShape, "PartNumberMarker retyped to integer to preserve the shipped C2J int API"); assertTrue(out.expectShape(ShapeId.from(NS + "#NextPartNumberMarker")) instanceof IntegerShape, @@ -189,7 +187,7 @@ void marksOverrideStreamingRequests() { StructureShape policy = StructureShape.builder().id(NS + "#PutBucketPolicyRequest").build(); StructureShape other = StructureShape.builder().id(NS + "#GetObjectRequest").build(); ServiceShape svc = s3Service("S3"); - Model out = S3Transforms.asTransform().apply(modelWith(svc, put, policy, other), svc); + Model out = new S3Transforms().transform(modelWith(svc, put, policy, other), svc); assertTrue(out.expectShape(put.getId()).hasTrait(OverrideStreamingTrait.class), "PutObjectAnnotationRequest is in REQUESTS_TO_OVERRIDE_STREAMING"); assertTrue(out.expectShape(policy.getId()).hasTrait(OverrideStreamingTrait.class), @@ -232,7 +230,7 @@ private static Model checksumModel(boolean withAlgorithmMember) { void marksChecksumMembersOnRequestWithChecksumAlgorithm() { Model m = checksumModel(true); ServiceShape svc = m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); StructureShape req = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); assertEquals("CRC32", req.getMember("ChecksumCRC32").orElseThrow().expectTrait(ChecksumMemberTrait.class).getValue()); @@ -249,7 +247,7 @@ void marksChecksumMembersOnRequestWithChecksumAlgorithm() { void doesNotMarkChecksumMembersWithoutChecksumAlgorithm() { Model m = checksumModel(false); ServiceShape svc = m.expectShape(ShapeId.from(NS + "#AmazonS3"), ServiceShape.class); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); StructureShape req = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); assertFalse(req.getMember("ChecksumCRC32").orElseThrow().hasTrait(ChecksumMemberTrait.class), "no ChecksumAlgorithm member => C2J does not flag the checksum members"); @@ -258,7 +256,7 @@ void doesNotMarkChecksumMembersWithoutChecksumAlgorithm() { @Test void addsExpiresStringToOutputAndDeprecatesExpires() { Model m = expiresModel(); - Model out = S3Transforms.asTransform().apply(m, expiresService(m)); + Model out = new S3Transforms().transform(m, expiresService(m)); assertTrue(out.getShape(ShapeId.from(NS + "#ExpiresString")).isPresent(), "ExpiresString string shape injected"); @@ -276,7 +274,7 @@ void addsExpiresStringToOutputAndDeprecatesExpires() { @Test void doesNotAddExpiresStringToInput() { Model m = expiresModel(); - Model out = S3Transforms.asTransform().apply(m, expiresService(m)); + Model out = new S3Transforms().transform(m, expiresService(m)); StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); assertFalse(input.getMember("ExpiresString").isPresent(), @@ -296,7 +294,7 @@ void appendsMissingBucketLocationConstraintRegions() { .addMember("us_west_2", "us-west-2") .build(); Model m = modelWith(svc, enumShape); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); software.amazon.smithy.model.shapes.EnumShape result = out.expectShape( ShapeId.from(NS + "#BucketLocationConstraint"), @@ -324,8 +322,8 @@ void bucketLocationConstraintExpansionIsIdempotent() { .addMember("us_west_2", "us-west-2") .build(); Model m = modelWith(svc, enumShape); - Model once = S3Transforms.asTransform().apply(m, svc); - Model twice = S3Transforms.asTransform().apply(once, svc); + Model once = new S3Transforms().transform(m, svc); + Model twice = new S3Transforms().transform(once, svc); software.amazon.smithy.model.shapes.EnumShape result = twice.expectShape( ShapeId.from(NS + "#BucketLocationConstraint"), software.amazon.smithy.model.shapes.EnumShape.class); @@ -346,7 +344,7 @@ void normalizesReplicationStatusCompleteToCompleted() { .addMember("COMPLETED", "COMPLETED") .build(); Model m = modelWith(svc, enumShape); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); java.util.List values = com.amazonaws.util.awsclientsmithygenerator.generators.model .EnumRenderer.getEnumValues(out.expectShape(ShapeId.from(NS + "#ReplicationStatus"))); @@ -360,7 +358,7 @@ void injectsGetObjectId2Only() { StructureShape getObjectOutput = StructureShape.builder().id(NS + "#GetObjectOutput") .addMember("ETag", ShapeId.from("smithy.api#String")).build(); Model m = modelWith(svc, getObjectOutput); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); assertTrue(out.getShape(ShapeId.from(NS + "#ObjectId2")).isPresent()); StructureShape outShape = out.expectShape(ShapeId.from(NS + "#GetObjectOutput"), StructureShape.class); @@ -402,7 +400,7 @@ private static ServiceShape s3ServiceOf(Model m) { @Test void injectsCustomizedAccessLogTagIntoRequestAsStringMapAppendedLast() { Model m = accessLogModel(); - Model out = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); + Model out = new S3Transforms().transform(m, s3ServiceOf(m)); StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); MemberShape tag = input.getMember("customizedAccessLogTag").orElseThrow(); @@ -426,7 +424,7 @@ void injectsCustomizedAccessLogTagIntoRequestAsStringMapAppendedLast() { @Test void stampsCustomizedAccessLogTagMarkerAndKeepsQueryParams() { Model m = accessLogModel(); - Model out = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); + Model out = new S3Transforms().transform(m, s3ServiceOf(m)); StructureShape input = out.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); MemberShape tag = input.getMember("customizedAccessLogTag").orElseThrow(); @@ -443,7 +441,7 @@ void stampsCustomizedAccessLogTagMarkerAndKeepsQueryParams() { @Test void doesNotInjectCustomizedAccessLogTagIntoOutput() { Model m = accessLogModel(); - Model out = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); + Model out = new S3Transforms().transform(m, s3ServiceOf(m)); StructureShape output = out.expectShape(ShapeId.from(NS + "#PutObjectOutput"), StructureShape.class); assertFalse(output.getMember("customizedAccessLogTag").isPresent(), @@ -458,7 +456,7 @@ void stampsEmbeddedErrorsTraitOnRequestInC2jSet() { StructureShape notInSet = StructureShape.builder().id(NS + "#SomeOtherRequest") .addMember("Bucket", ShapeId.from("smithy.api#String")).build(); Model m = modelWith(svc, inSet, notInSet); - Model out = S3Transforms.asTransform().apply(m, svc); + Model out = new S3Transforms().transform(m, svc); StructureShape marked = out.expectShape(ShapeId.from(NS + "#CreateSessionRequest"), StructureShape.class); assertTrue(marked.hasTrait(EmbeddedErrorsTrait.class), @@ -471,8 +469,8 @@ void stampsEmbeddedErrorsTraitOnRequestInC2jSet() { @Test void accessLogTagInjectionIsIdempotent() { Model m = accessLogModel(); - Model once = S3Transforms.asTransform().apply(m, s3ServiceOf(m)); - Model twice = S3Transforms.asTransform().apply(once, s3ServiceOf(once)); + Model once = new S3Transforms().transform(m, s3ServiceOf(m)); + Model twice = new S3Transforms().transform(once, s3ServiceOf(once)); StructureShape input = twice.expectShape(ShapeId.from(NS + "#PutObjectRequest"), StructureShape.class); long count = input.getAllMembers().keySet().stream() diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java index 4e41dcdb333..4b20eafdac2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SourceRegionTransformTest.java @@ -46,7 +46,7 @@ private static ServiceShape service(Model m) { @Test void injectsSourceRegionIntoRdsRequest() { Model m = modelWithOp("RDS", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotRequest"); - Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + Model out = new SourceRegionTransform().transform(m, service(m)); StructureShape req = out.expectShape( ShapeId.from("com.example#CopyDBClusterSnapshotRequest"), StructureShape.class); @@ -58,7 +58,7 @@ void injectsSourceRegionIntoRdsRequest() { @Test void noOpForUntargetedOperation() { Model m = modelWithOp("RDS", "DescribeDBClusters", "DescribeDBClustersRequest"); - Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + Model out = new SourceRegionTransform().transform(m, service(m)); assertTrue(out.expectShape(ShapeId.from("com.example#DescribeDBClustersRequest"), StructureShape.class).getMember("SourceRegion").isEmpty()); } @@ -66,15 +66,14 @@ void noOpForUntargetedOperation() { @Test void noOpForUntargetedService() { Model m = modelWithOp("SomeOther", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotRequest"); - Model out = SourceRegionTransform.asTransform().apply(m, service(m)); - assertSame(m, out); + assertFalse(new SourceRegionTransform().shouldRun(service(m))); } @Test void idempotent_doesNotDuplicateExistingMember() { Model m = modelWithOp("RDS", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotRequest"); - Model once = SourceRegionTransform.asTransform().apply(m, service(m)); - Model twice = SourceRegionTransform.asTransform().apply(once, service(once)); + Model once = new SourceRegionTransform().transform(m, service(m)); + Model twice = new SourceRegionTransform().transform(once, service(once)); long count = twice.expectShape(ShapeId.from("com.example#CopyDBClusterSnapshotRequest"), StructureShape.class).members().stream() .filter(mem -> mem.getMemberName().equals("SourceRegion")).count(); @@ -84,7 +83,7 @@ void idempotent_doesNotDuplicateExistingMember() { @Test void injectsSourceRegionIntoDocDbRequest() { Model m = modelWithOp("DocDB", "CreateDBCluster", "CreateDBClusterMessage"); - Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + Model out = new SourceRegionTransform().transform(m, service(m)); assertTrue(out.expectShape(ShapeId.from("com.example#CreateDBClusterMessage"), StructureShape.class).getMember("SourceRegion").isPresent()); } @@ -92,7 +91,7 @@ void injectsSourceRegionIntoDocDbRequest() { @Test void injectsSourceRegionIntoNeptuneRequest() { Model m = modelWithOp("Neptune", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotMessage"); - Model out = SourceRegionTransform.asTransform().apply(m, service(m)); + Model out = new SourceRegionTransform().transform(m, service(m)); assertTrue(out.expectShape(ShapeId.from("com.example#CopyDBClusterSnapshotMessage"), StructureShape.class).getMember("SourceRegion").isPresent()); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java index 60196800cb1..91eec74c160 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SqsTransformsTest.java @@ -39,7 +39,7 @@ void addsValuesToEnumShape() { Model m = Model.assembler().addShapes(enumShape, sqsService()).assemble().unwrap(); ServiceShape svc = m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); - Model out = SqsTransforms.asTransform().apply(m, svc); + Model out = new SqsTransforms().transform(m, svc); List values = EnumRenderer.getEnumValues( out.expectShape(ShapeId.from("com.example#QueueAttributeName"))); assertTrue(values.containsAll(ADDED)); @@ -58,7 +58,7 @@ void addsValuesToStringEnumTrait() { Model m = Model.assembler().addShapes(s, sqsService()).assemble().unwrap(); ServiceShape svc = m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); - Model out = SqsTransforms.asTransform().apply(m, svc); + Model out = new SqsTransforms().transform(m, svc); List values = EnumRenderer.getEnumValues( out.expectShape(ShapeId.from("com.example#QueueAttributeName"))); assertTrue(values.containsAll(ADDED)); @@ -81,8 +81,8 @@ void idempotent() { Model m = Model.assembler().addShapes(enumShape, sqsService()).assemble().unwrap(); ServiceShape svc = m.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); - Model once = SqsTransforms.asTransform().apply(m, svc); - Model twice = SqsTransforms.asTransform().apply(once, svc); + Model once = new SqsTransforms().transform(m, svc); + Model twice = new SqsTransforms().transform(once, svc); long senderId = EnumRenderer.getEnumValues( twice.expectShape(ShapeId.from("com.example#QueueAttributeName"))) .stream().filter("SenderId"::equals).count(); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java index 6143b4b6b0c..78960b1adcc 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/transforms/SupportsPresigningTransformTest.java @@ -76,7 +76,7 @@ private static ServiceTrait pollyServiceTrait() { @Test void queryXmlService_stampsEveryOperation() { Model m = twoOpModel(new AwsQueryTrait(), null, "GetUser", "CreateUser"); - Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + Model out = new SupportsPresigningTransform().transform(m, service(m)); assertTrue(stamped(out, "GetUser"), "query operation must be stamped"); assertTrue(stamped(out, "CreateUser"), "query operation must be stamped"); } @@ -84,7 +84,7 @@ void queryXmlService_stampsEveryOperation() { @Test void ec2Service_stampsEveryOperation() { Model m = twoOpModel(new Ec2QueryTrait(), null, "DescribeThings", "RunThings"); - Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + Model out = new SupportsPresigningTransform().transform(m, service(m)); assertTrue(stamped(out, "DescribeThings"), "ec2 operation must be stamped"); assertTrue(stamped(out, "RunThings"), "ec2 operation must be stamped"); } @@ -94,7 +94,7 @@ void queryXmlService_stampsUnitInputOperation() { // The regression: a Unit-input query op (e.g. iam GetAccountSummary) cannot stamp the shared // Unit input shape, but the operation itself carries the trait so decl+impl stay symmetric. Model m = opPlusUnitInputModel(new AwsQueryTrait(), "ListUsers", "GetAccountSummary"); - Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + Model out = new SupportsPresigningTransform().transform(m, service(m)); assertTrue(stamped(out, "ListUsers"), "normal-input query operation must be stamped"); assertTrue(stamped(out, "GetAccountSummary"), "Unit-input query operation must be stamped on the operation itself"); @@ -104,7 +104,7 @@ void queryXmlService_stampsUnitInputOperation() { void pollyService_stampsOnlySynthesizeSpeechOperation() { Model m = twoOpModel(RestJson1Trait.builder().build(), pollyServiceTrait(), "SynthesizeSpeech", "DescribeVoices"); - Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + Model out = new SupportsPresigningTransform().transform(m, service(m)); assertTrue(stamped(out, "SynthesizeSpeech"), "Polly SynthesizeSpeech must be stamped"); assertFalse(stamped(out, "DescribeVoices"), "Polly must stamp only SynthesizeSpeech, not other operations"); @@ -113,7 +113,7 @@ void pollyService_stampsOnlySynthesizeSpeechOperation() { @Test void plainRestJsonService_stampsNothing() { Model m = twoOpModel(RestJson1Trait.builder().build(), null, "GetThing", "PutThing"); - Model out = SupportsPresigningTransform.asTransform().apply(m, service(m)); + Model out = new SupportsPresigningTransform().transform(m, service(m)); assertSame(m, out, "a non-query, non-Polly rest-json service must be left untouched"); assertFalse(stamped(out, "GetThing")); assertFalse(stamped(out, "PutThing")); From 379184add897edb362e4d1c3dd8770236b0cc217 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 2 Sep 2026 15:54:06 -0400 Subject: [PATCH 44/44] slash some comments out --- .../generators/model/ModelTransform.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java index 105367c4451..cb8157f79bc 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java @@ -10,20 +10,11 @@ /** * A model-to-model transform applied before code generation. Transforms run in sequence, * each receiving the previous transform's output (or the original model for the first). - * - *

{@link #shouldRun} is the service-level gate and defaults to {@code false}: a transform runs - * only when it explicitly opts in by overriding it — a service check, or {@code true} for transforms - * that apply to every service. This fails closed, so a transform added without a gate silently - * no-ops instead of running for every service and mutating models it was never meant to touch. When - * {@code shouldRun} returns true, {@code transform} may assume it applies and need not re-check the - * service. */ public interface ModelTransform { /** - * Whether this transform applies to the given service. Evaluated by the pipeline before - * {@link #transform}; a false result skips the transform. Defaults to {@code false}, so a - * transform must override this to run — either a service check or {@code true} to run always. + * Whether this transform applies to the given service. * * @param service the service shape being generated * @return true if {@link #transform} should be invoked for this service @@ -33,8 +24,7 @@ default boolean shouldRun(ServiceShape service) { } /** - * Applies this transform to the model. Only invoked when {@link #shouldRun} returns true, - * so implementations need not re-check the service. + * Applies this transform to the model. * * @param model the current model (may have been modified by earlier transforms) * @param service the service shape being generated