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 d10a51b689b1..5b5c391d8e6a 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,54 +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( - "s3", Map.of("CopyObjectResult", "CopyObjectResultDetails"), - "accessanalyzer", Map.of("GeneratedPolicyResult", "GeneratedPolicyResults"), - "cloudsearchdomain", Map.of("SearchResult", "SearchResultDetails") - ); - - /** - * 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. - */ - 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/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 12d7e3ce8cd2..19259e26f517 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,37 +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, 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 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(""); } @@ -316,11 +293,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, + * descending only through further list/map shapes (bounded by nesting depth). Lets a member + * typed {@code Map>} reach {@code Leaf}'s header. C2J parity. + * + *

{@code @sparse} adds {@code } at each sparse nesting level. + */ + 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 (). + // C2J parity. + 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 - * 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 4901d639a661..9a2d06e0d1fb 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,11 +51,24 @@ 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 {"); - // 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) + " };"; @@ -74,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(""); @@ -115,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++) { @@ -140,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); @@ -225,6 +235,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/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 03221bf1fc60..918a5fbfcc20 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,15 +7,20 @@ 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; 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; +import java.util.Optional; /** * Renders C++ accessor methods (Get/Set/With/Add) and private member fields @@ -102,7 +107,11 @@ public void renderPublicAccessors(CppWriter writer) { writer.write("inline const $L& Get$L() const { return $L; }", cppType, methodName, fieldName); } - if (emitHasBeenSet) { + // 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); } @@ -116,19 +125,30 @@ 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. + 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); 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 { 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); @@ -255,10 +275,23 @@ 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 + * 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) { 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;"); @@ -272,11 +305,34 @@ 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 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(""); + 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 - * 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) { @@ -299,20 +355,58 @@ 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: 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 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: 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()) + && 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); } /** * 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); @@ -336,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 d3eeee702761..1c9a1a7051b6 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,21 @@ 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.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; 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; +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; @@ -30,20 +44,30 @@ 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() - // Future: S3Transforms.asTransform(), Ec2Transforms.asTransform(), etc. + 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()); @@ -52,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/ModelGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java index f8e4eb7f84f2..52eba91a696f 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,7 +7,9 @@ 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; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.OutgoingEventStreamRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestRenderer; @@ -64,11 +66,13 @@ 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)); + 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/ModelTransform.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java index 396fb3be6bf0..cb8157f79bce 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,16 +8,21 @@ 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 { + /** + * 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 + */ + default boolean shouldRun(ServiceShape service) { + return false; + } + /** * Applies this transform to the model. * @@ -25,5 +30,5 @@ public interface ModelTransform { * @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/ProtocolResolver.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ProtocolResolver.java index 83b86a5c8541..9e33439f476f 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 0486fdfd94a8..3ad5a649f0de 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 5d57f27da337..237f318c3ff1 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,9 +5,9 @@ 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; import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; @@ -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,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 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, @@ -79,7 +86,9 @@ public record ClassifiedShapes( List subObjects, List enums, List eventStreamHandlers, - List outgoingEventStreams + List outgoingEventStreams, + List blobPayloadEvents, + Set resultOutputIds ) {} private ShapeClassifier() {} @@ -94,7 +103,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<>(); @@ -109,12 +117,13 @@ 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)) { - // 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. + // 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 (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 -> { @@ -151,37 +160,75 @@ 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 = 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 = 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 + // standalone .h is dead public API 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); - 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"), routed here + // before the generic structure branch. + blobPayloadEvents.add(shape); } else if (shape.hasTrait(ErrorTrait.class)) { if (isModeledException(shape.asStructureShape().get(), protocol)) { subObjects.add(shape); } } else if (shape.isStructureShape() || shape.isUnionShape()) { - subObjects.add(shape); + // Skip: + // - @customRendered shapes (emitted by a dedicated renderer, e.g. DynamoDbRenderer + // 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() + && shape.members().isEmpty(); + boolean incomingUnion = incomingEventStreamUnionIds.contains(id); + if (!customRendered && !emptyEventStruct && !incomingUnion) { + subObjects.add(shape); + } } } return new ClassifiedShapes(requests, results, subObjects, enums, - eventStreamHandlers, outgoingEventStreams); + eventStreamHandlers, outgoingEventStreams, blobPayloadEvents, outputShapeIds); } /** - * 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(); @@ -212,6 +259,31 @@ private static boolean hasRawStreamingPayload(StructureShape shape, Model model) return false; } + /** + * 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())) { + 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) { + return shape.getAllMembers().values().stream() + .filter(member -> member.hasTrait(EventPayloadTrait.class) + && model.expectShape(member.getTarget()).isBlobShape()) + .map(MemberShape::getMemberName) + .findFirst(); + } + /** * 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/ShapeRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeRenderer.java index 47ae2d878c97..a5cdb1c8718c 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/TransformPipeline.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java index 4259807bbd11..fcaae7fad130 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/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 8baef186b522..a406574efafc 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,10 @@ 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 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: case STREAMING_RESULT_SOURCE: case EVENT_HANDLER_SOURCE: @@ -166,7 +173,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 +212,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 9996841ba713..ce9f625a1452 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; @@ -88,10 +89,21 @@ public List serdeIncludes(FileKind kind) { case SUBOBJECT_HEADER: case RESULT_HEADER: return List.of(); - // 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: + // 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", + "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", + "numeric", + "utility"); + // All source kinds share one union (supersets allowed). Only #includes widen; usings unchanged. + case SUBOBJECT_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: case EVENT_HANDLER_SOURCE: @@ -157,6 +169,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 +183,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 +202,20 @@ 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, shape, model); + } + // DumpBodyToUrl stub for presigning-capable operations; body serialization pending schema serde. + if (operation.hasTrait(SupportsPresigningTrait.class)) { writer.write(""); - writeAddQueryStringParametersImpl(writer, className); + 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/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 d7002148dc06..c3c375ce9e56 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,9 @@ 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 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; @@ -18,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))}. @@ -148,21 +150,30 @@ default boolean hasTargetHeader() { } /** - * 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 this protocol honors HTTP binding traits ({@code @httpHeader} / + * {@code @httpPrefixHeaders} / {@code @httpQuery} / {@code @httpQueryParams}) by serializing + * those members onto the wire. + * + *

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} 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; @@ -186,15 +197,33 @@ 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 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 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;"); + } + 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 b4409b578cac..7d9cd84642ef 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; @@ -95,8 +96,20 @@ 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 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", + "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", + "numeric"); + case SUBOBJECT_SOURCE: case RESULT_SOURCE: case STREAMING_RESULT_SOURCE: case EVENT_HANDLER_SOURCE: @@ -203,18 +216,8 @@ 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 DumpBodyToUrl override decl is emitted protocol-agnostically by RequestRenderer + // (gated on SupportsPresigningTrait); only the impl below is protocol-specific. } @Override @@ -230,10 +233,14 @@ public void writeRequestMethodImpls(CppWriter writer, String className, } if (RequestBindings.hasQueryStringMembers(shape, model)) { writer.write(""); - writeAddQueryStringParametersImpl(writer, className); + writeAddQueryStringParametersImpl(writer, className, shape, model); + } + // 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()); }", + className); } - 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/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 4eda33f32fde..9522dce53d1d 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; @@ -74,9 +75,21 @@ 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 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", + "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: @@ -98,9 +111,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: @@ -148,6 +164,10 @@ public void writeRequestMethodDecls(CppWriter writer, String exportMacro, writer.write(""); writeAddQueryStringParametersDecl(writer, exportMacro); } + if (shape.hasTrait(EmbeddedErrorsTrait.class)) { + writer.write(""); + writeHasEmbeddedErrorDecl(writer, exportMacro); + } } @Override @@ -163,7 +183,36 @@ 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(""); + 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: 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, () -> { + 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/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 000000000000..d7aecfaaf27a --- /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,58 @@ +/** + * 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 (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 { + + 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/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 000000000000..8f5fa136b1b7 --- /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,114 @@ +/** + * 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 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}. + * + *

{@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 { + + 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 (whitespace-collapsed {@code @documentation}), matching + * C2J {@code EventHeader.vm}. + */ + 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/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 f256588f006c..805134b4a40f 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; @@ -28,13 +24,11 @@ import java.util.Optional; /** - * 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. - * - *

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 { @@ -61,7 +55,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 +86,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,12 +111,14 @@ 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(), () -> { - // EventType enum StringBuilder enumBody = new StringBuilder("enum class ") .append(opName).append("EventType { INITIAL_RESPONSE, "); for (MemberShape event : events) { @@ -149,12 +129,16 @@ 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) { 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(""); @@ -246,16 +230,22 @@ 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); }); 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()); @@ -263,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());"); @@ -293,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);"); @@ -309,8 +297,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;"); }); } @@ -321,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;"); @@ -370,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) { @@ -408,12 +399,11 @@ 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 - * 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() @@ -500,107 +490,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/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 4b9e8f410327..94b46af17890 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 8e21fe78903d..a8f0d67989d9 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 df03be05ff95..d7e7679c2b27 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 new file mode 100644 index 000000000000..12bcdd43ef1f --- /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,160 @@ +/** + * 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's + * {@code GetRequestSpecificHeaders()}, byte-matching C2J {@code ModelClassHeaderMembersSource.vm}. + * + *

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}. + * + *

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 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 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 -> + 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 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()); + 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 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); + 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 000000000000..9ff723772d94 --- /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,178 @@ +/** + * 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's + * {@code AddQueryStringParameters(Aws::Http::URI&)}, byte-matching C2J + * {@code AddQueryStringParametersToRequest.vm}. + * + *

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). + * + *

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). + * + *

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 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()) { + 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 emits the x- filter block after this loop + // instead of serializing as a normal map. + 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 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()); + 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: 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); + 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 → 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); + 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/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 3751644a1187..37ef5369a2bd 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,11 +14,19 @@ 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.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; +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; 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; @@ -26,6 +34,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; @@ -60,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; @@ -80,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); @@ -132,24 +140,58 @@ private void renderHeader(CppWriterDelegator writerDelegator, if (streamingRequest) { writer.write("inline virtual bool IsEventStreamRequest() const override { return true; }"); } + // 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; }"); + } if (streamingResponse) { 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 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(); + 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); renderContentMd5Decl(writer, operation); + renderSignBodyDecl(writer, shape, operation); + + // 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()); + } + 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(""); @@ -187,6 +229,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(""); @@ -228,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) { @@ -247,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); @@ -272,6 +315,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 +339,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(""); }); @@ -300,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<>(); @@ -343,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) { @@ -373,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); @@ -405,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) { @@ -452,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)) { @@ -465,6 +510,18 @@ private void renderContentMd5Decl(CppWriter writer, OperationShape operation) { } } + /** + * 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()) { + 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); @@ -476,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)) { @@ -494,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) { @@ -529,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(); @@ -546,7 +599,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 +643,49 @@ 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 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. + 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/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 dbd565fe9b57..930582bcb951 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; @@ -12,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; @@ -48,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()) { @@ -64,20 +64,18 @@ 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"); - // 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); @@ -113,6 +111,12 @@ private void renderHeader(CppWriterDelegator writerDelegator, MemberRenderer.renderRequestIdAccessors(writer, className); } + // 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); + } + writer.write("inline Aws::Http::HttpResponseCode GetHttpResponseCode() const { return m_HttpResponseCode; }"); writer.write(""); @@ -121,16 +125,23 @@ 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;"); } + 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(""); }); @@ -139,7 +150,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 -> { @@ -158,13 +169,12 @@ 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) { - 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 -> { @@ -209,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) @@ -231,6 +240,12 @@ private void renderStreamingHeader(CppWriterDelegator writerDelegator, MemberRenderer.renderRequestIdAccessors(writer, className); + // 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); + } + writer.write("inline Aws::Http::HttpResponseCode GetHttpResponseCode() const { return m_HttpResponseCode; }"); writer.write(""); @@ -241,10 +256,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(""); }); @@ -252,12 +274,12 @@ 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) { - 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/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 837c62e72f29..8c9d273cc79e 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,20 +24,21 @@ 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; } @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) { @@ -48,12 +51,19 @@ 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 "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) { + includes.add("aws/core/utils/memory/stl/AWSString.h"); + } for (String memberInc : CppTypeMapper.getIncludesForShape(shape, ctx.model(), ctx.smithyServiceName())) { includes.add(memberInc); } @@ -72,8 +82,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); } @@ -84,18 +94,42 @@ 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). - if (!shape.getAllMembers().isEmpty()) { + // 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) .wideIntegers(ctx.protocolTraits().widensIntegers()); - writer.write(""); - members.renderPublicAccessors(writer); + if (hasMembers) { + writer.write(""); + members.renderPublicAccessors(writer); + } + if (stampRequestId) { + // 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(); 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 like + // MemberRenderer's 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/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 000000000000..5a7689c5e991 --- /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 000000000000..8889498c04cb --- /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,142 @@ +/** + * 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 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 { + + 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 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"; + 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/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 000000000000..2be23779b59e --- /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,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.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.transform.ModelTransformer; + +import java.util.Map; +import java.util.Optional; + +/** + * 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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + return "accessanalyzer".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @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"); + + 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; + } + Protocol protocol = ProtocolResolver.resolve(service, renamed); + Optional updated = TransformSupport.renameMember( + 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/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 000000000000..bce200d44b5e --- /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,43 @@ +/** + * 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 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"); + + private final Map headers; + + public AdditionalRequestHeadersTrait(Map headers) { + super(ID, SourceLocation.NONE); + this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); + } + + 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/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 000000000000..9162529c6455 --- /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 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; +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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + return "api-gateway".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + 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", protocol).orElse(struct); + TransformSupport.renameMember(afterBody, "headers", "requestHeaders", protocol) + .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 000000000000..b6dea0be8430 --- /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 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; +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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + return "apigatewayv2".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + 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", protocol)) + .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/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 000000000000..d791d9241208 --- /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,22 @@ +/** + * 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 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"); + + 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/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 000000000000..2f587d666dba --- /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,23 @@ +/** + * 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 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"); + + 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 000000000000..d40bd0e5da0c --- /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,56 @@ +/** + * 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 {@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 implements ModelTransform { + + private static final String WRITE_GET_OBJECT_RESPONSE = "WriteGetObjectResponse"; + + @Override + public boolean shouldRun(ServiceShape service) { + return true; + } + + @Override + public Model transform(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/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 000000000000..1cbcf123c46f --- /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,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.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * 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 { + + 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/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 000000000000..923fcdd3d529 --- /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,24 @@ +/** + * 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 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"); + + 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/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 000000000000..d2793ad40a27 --- /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,43 @@ +/** + * 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 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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + return "dynamodb".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + 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/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 000000000000..e225608c11b7 --- /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,127 @@ +/** + * 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.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +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 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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + return "ec2".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + return renameResultShapesToResponse( + addSecureBlobUserData(addSpotInstanceStateDisabled(model))); + } + + /** + * 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) + .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()) { + 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"); + 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()) { + 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()) { + return model; + } + return ModelTransformer.create().renameShapes(model, renames); + } + + private static Model addSpotInstanceStateDisabled(Model 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/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 000000000000..1165452bf056 --- /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,22 @@ +/** + * 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 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"); + + 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/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 000000000000..4675df14d8e3 --- /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,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 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 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 implements ModelTransform { + + private static final String GLACIER_VERSION_HEADER = "x-amz-glacier-version"; + + @Override + public boolean shouldRun(ServiceShape service) { + return "glacier".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + 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 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()) { + 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/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 587e926d0474..99040d3eab7d 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; @@ -21,10 +22,12 @@ 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; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -33,64 +36,94 @@ * 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 that skip the "body" -> "requestBody" member rename. - * These services use "body" as a meaningful domain member (e.g., 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", "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 (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( - "apigateway" + "api-gateway" ); - private GlobalTransforms() {} + /** + * 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"; - // 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/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. * - * 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); + Protocol protocol = ProtocolResolver.resolve(service, model); + Set inputIds = new HashSet<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + inputIds.add(op.getInputShape()); } - if ("headers".equals(memberName)) { - if (HEADERS_RENAME_SKIP_SERVICES.contains(smithyServiceName)) return Optional.empty(); - return Optional.of("headerValues"); + 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(), protocol); + if (next.isPresent()) { + current = next.get(); + changed = true; + } + } + if (changed) { + updated.add(current); + } + }); } - if ("Headers".equals(memberName)) { - return Optional.of("headerValues"); + if (updated.isEmpty()) { + return model; } - return Optional.empty(); + 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 (struct.getMember("headers").isPresent() + && !HEADERS_RENAME_SKIP_SERVICES.contains(smithyServiceName)) { + out.add(Map.entry("headers", "headerValues")); + } + if (struct.getMember("Headers").isPresent()) { + out.add(Map.entry("Headers", "headerValues")); + } + return out; } /** - * 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 @@ -99,7 +132,7 @@ public static Optional getReservedMemberRename(String memberName, String 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)); @@ -113,26 +146,45 @@ private static void addReachableFrom(ShapeId root, Walker walker, Model model, S } /** - * Returns this class as a ModelTransform. + * 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. * - *

Runs {@link #dropDeprecatedMembers} first (so reachability filtering sees the pruned - * model and orphaned targets drop out), then {@link #injectResponseMetadata}. + * @param model the Smithy model + * @param service the service whose operations are being generated + * @return the service's non-deprecated operations */ - public static ModelTransform asTransform() { - return (model, service) -> injectResponseMetadata(dropDeprecatedMembers(model, service), service); + public static List nonDeprecatedOperations(Model model, ServiceShape service) { + return TopDownIndex.of(model).getContainedOperations(service).stream() + .filter(op -> !op.hasTrait(DeprecatedTrait.class)) + .collect(Collectors.toList()); } /** - * 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. + * Returns this class as a ModelTransform: {@link #dropDeprecatedMembers} first (so reachability + * sees the pruned model), then {@link #renameReservedRequestMembers}, then + * {@link #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); + } + + /** + * 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) @@ -151,27 +203,34 @@ public static Model dropDeprecatedMembers(Model model, ServiceShape service) { } /** - * For awsQuery / ec2Query services, 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. + * 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 - * @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: 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() @@ -180,24 +239,27 @@ 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); - 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); } 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, 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() - .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 000000000000..3da6a97ee654 --- /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,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.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 implements ModelTransform { + + private static final ShapeId UNIT = ShapeId.from("smithy.api#Unit"); + + @Override + public boolean shouldRun(ServiceShape service) { + return "lambda".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + 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/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 000000000000..c89ba423ff88 --- /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,24 @@ +/** + * 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 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"); + + 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 000000000000..fc212625b918 --- /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,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 java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 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 implements ModelTransform { + + private static final Map> LONG_POLLING_OPERATIONS = Map.of( + "sqs", Set.of("ReceiveMessage"), + "sfn", Set.of("GetActivityTask"), + "swf", Set.of("PollForActivityTask", "PollForDecisionTask") + ); + + @Override + public boolean shouldRun(ServiceShape service) { + return LONG_POLLING_OPERATIONS.containsKey( + ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + Set longPollOps = + LONG_POLLING_OPERATIONS.get(ServiceNameUtil.getSmithyServiceName(service, null)); + 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/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 000000000000..518da95e340f --- /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,23 @@ +/** + * 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 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"); + + 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/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 000000000000..57bcc9ece021 --- /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,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 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 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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + return "s3-control".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + 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/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 000000000000..ae0793ee0fdc --- /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,417 @@ +/** + * 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.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; +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; +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 software.amazon.smithy.model.traits.HttpQueryParamsTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; +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; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + String name = ServiceNameUtil.getSmithyServiceName(service, null); + return "s3".equals(name) || "s3-crt".equals(name); + } + + @Override + public Model transform(Model model, ServiceShape service) { + Model result = markEmbeddedErrors(injectAccessLogTagQuery(normalizeReplicationStatus( + expandBucketLocationConstraint(hackGetObjectResult( + addExpiresCustomization(renameCopyObjectResult( + retypePartNumberMarkersToInteger(model), service), service)))), service)); + result = markOverrideStreaming(result); + return markChecksumMembers(result, service); + } + + // 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"); + + 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 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"), + 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 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 -> + CHECKSUM_MEMBERS_ENUMS.containsKey(m.getTarget().getName()) + && !m.hasTrait(ChecksumMemberTrait.class)); + if (needsStamp) { + // Re-add only the checksum members with the marker; addMember replaces in place, so + // 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()); + 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()); + } + } + if (replacements.isEmpty()) { + return model; // no qualifying request (idempotent / other model). + } + return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); + } + + // 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", + "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 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 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) + .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) + // @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. + .addTrait(new CustomizedAccessLogTagTrait()) + .build()) + .build()); + } + return model.toBuilder().addShapes(replacements.toArray(new Shape[0])).build(); + } + + // 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()) + .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; + // the other 18 C2J regions are already present upstream. + private static final List MISSING_REGIONS = List.of("us-iso-west-1", "us-east-1"); + + private static Model expandBucketLocationConstraint(Model model) { + return TransformSupport.appendEnumEntriesByName( + model, "BucketLocationConstraint", regionNameValueMap()); + } + + private static Map regionNameValueMap() { + LinkedHashMap map = new LinkedHashMap<>(); + for (String region : MISSING_REGIONS) { + map.put(region.replace('-', '_'), region); + } + return map; + } + + // C2J's GetObjectResult carries an x-amz-id-2 header member (Id2) plus the standard RequestId. + // 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"); + 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()) { + return model; // already injected (idempotent) — or upstream added it. + } + ShapeId id2ShapeId = ShapeId.fromParts(ns, "ObjectId2"); + StringShape id2Shape = StringShape.builder().id(id2ShapeId).build(); + + StructureShape withId2 = output.toBuilder() + .addMember("Id2", id2ShapeId, mb -> mb.addTrait(new HttpHeaderTrait("x-amz-id-2"))) + .build(); + + return model.toBuilder().addShapes(id2Shape, withId2).build(); + } + + // 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"); + 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."); + } + 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 = + "Deprecated: Please use ExpiresString instead. " + System.lineSeparator() + " * "; + + 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 output shape carries Expires: nothing more 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) { + // Only customize structs that lack ExpiresString (idempotent). + if (struct.getMember("ExpiresString").isEmpty()) { + MemberShape expires = struct.getAllMembers().get("Expires"); + 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, + 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(); + } + + // 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"); + + 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); + 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/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 000000000000..947dfe257baa --- /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,78 @@ +/** + * 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 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 implements ModelTransform { + + 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") + ); + + @Override + public boolean shouldRun(ServiceShape service) { + return TARGETS.containsKey(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @Override + public Model transform(Model model, ServiceShape service) { + String serviceName = ServiceNameUtil.getSmithyServiceName(service, null); + Set operations = TARGETS.get(serviceName); + + List updated = new ArrayList<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + 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()); + } + }); + } + } + + 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 000000000000..32c69f2ed594 --- /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,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 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 java.util.List; + +/** + * 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 implements ModelTransform { + + private static final String ENUM_NAME = "QueueAttributeName"; + private static final List ADDED_VALUES = List.of( + "SentTimestamp", "ApproximateFirstReceiveTimestamp", "ApproximateReceiveCount", "SenderId"); + + @Override + public boolean shouldRun(ServiceShape service) { + return "sqs".equals(ServiceNameUtil.getSmithyServiceName(service, null)); + } + + @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/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 000000000000..76e2ba8a735f --- /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,23 @@ +/** + * 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 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"); + + 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 000000000000..78c8337147dc --- /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,53 @@ +/** + * 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 java.util.ArrayList; +import java.util.List; + +/** + * 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 implements ModelTransform { + + @Override + public boolean shouldRun(ServiceShape service) { + return true; + } + + @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)); + if (!queryLike && !polly) { + return model; + } + List updated = new ArrayList<>(); + for (OperationShape op : TopDownIndex.of(model).getContainedOperations(service)) { + // 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()); + } + } + 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/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 000000000000..0ff68d7beb3f --- /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,23 @@ +/** + * 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 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"); + + public TopLevelHostIdTrait() { + 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/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 000000000000..5a60a828ca57 --- /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,262 @@ +/** + * 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 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; +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.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. + */ +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. 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. + * + * @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 + */ + 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()); + } + + /** + * Appends {@code name -> value} enum entries, allowing wire values that are not + * 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 + * @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()); + } + + /** + * 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) + .flatMap(shape -> appendValues(shape, values)) + .map(updated -> model.toBuilder().addShape(updated).build()) + .orElse(model); + } + + /** + * 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) + .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()) { + 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 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 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) { + 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()) { + 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) { + wireNameTraits.forEach(b::addTrait); + } + }); + } + return Optional.of(builder.build()); + } + + /** + * 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 — + * {@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, + 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/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 000000000000..174eb686901e --- /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 000000000000..299b8e21f876 --- /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 000000000000..1ed20e24d26e --- /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 000000000000..f0a92e47095a --- /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 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 d5e1754668d7..9232e21bf082 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") @@ -641,4 +634,162 @@ void getIncludesForShape_withMapMember_includesMapKeyAndValue() { assertTrue(includes.contains("")); assertTrue(includes.contains("")); } + + @Test + void getIncludesForShape_withNestedMapOfMap_includesLeafStructHeader() { + // 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() + .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); + } } 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 35dd58fd79a5..8dae2e12f552 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") @@ -327,4 +326,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()); + } } 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 000000000000..6545cc4e58f7 --- /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,84 @@ +/** + * 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): {@code Aws::Vector} payload, no JSON serde, 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/EventStreamRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java index f3638429c042..82925e48bd7d 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,60 @@ 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 (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() + .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 +158,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 +206,18 @@ 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, 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); + // 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 @@ -205,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); @@ -284,4 +316,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/GlobalTransformsTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/GlobalTransformsTest.java index d4be7bdda094..80868cd9e888 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,97 +8,169 @@ 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.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); + /** 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_skippedForApiGateway() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "apigateway"); - assertTrue(result.isEmpty()); + private static StructureShape input(Model m) { + return m.expectShape(ShapeId.from("com.example#DoThingRequest"), StructureShape.class); } @Test - void reservedMemberRename_body_skippedForBedrockRuntime() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "bedrock-runtime"); - assertTrue(result.isEmpty()); + void reservedRename_body_becomesRequestBody_forNonSkippedService() { + Model m = inputModel("Security IR", "body"); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); + assertFalse(input(out).getMember("body").isPresent()); + assertTrue(input(out).getMember("requestBody").isPresent()); } @Test - void reservedMemberRename_body_skippedForAmplifyUiBuilder() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "amplifyuibuilder"); - assertTrue(result.isEmpty()); + void reservedRename_body_skippedForBedrockRuntime() { + Model m = inputModel("Bedrock Runtime", "body"); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("body").isPresent(), "skip-listed service keeps body"); } @Test - void reservedMemberRename_body_skippedForApiGateway2() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "apigateway2"); - 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 = new GlobalTransforms().transform(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("body").isPresent(), "api-gateway must be skipped"); } @Test - void reservedMemberRename_body_skippedForGlacier() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "glacier"); - assertTrue(result.isEmpty()); + void reservedRename_headers_becomesHeaderValues_forNonSkippedService() { + Model m = inputModel("Kinesis", "headers"); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("headerValues").isPresent()); + assertFalse(input(out).getMember("headers").isPresent()); } @Test - void reservedMemberRename_body_skippedForRepostSpace() { - Optional result = GlobalTransforms.getReservedMemberRename("body", "repostspace"); - assertTrue(result.isEmpty()); + void reservedRename_capitalHeaders_alwaysRenamed() { + Model m = inputModel("API Gateway", "Headers"); + Model out = new GlobalTransforms().transform(m, serviceOf(m, "Example")); + assertTrue(input(out).getMember("headerValues").isPresent()); } @Test - void reservedMemberRename_headers_becomesHeaderValues() { - 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 = 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"); } @Test - void reservedMemberRename_headers_skippedForApiGateway() { - Optional result = GlobalTransforms.getReservedMemberRename("headers", "apigateway"); - assertTrue(result.isEmpty()); + void reservedRename_collision_throws() { + Model m = inputModel("Kinesis", "body", "requestBody"); + assertThrows(IllegalStateException.class, + () -> new GlobalTransforms().transform(m, serviceOf(m, "Example"))); } @Test - void reservedMemberRename_headers_notSkippedForBedrockRuntime() { - Optional result = GlobalTransforms.getReservedMemberRename("headers", "bedrock-runtime"); - assertEquals(Optional.of("headerValues"), result); + void reservedRename_jsonService_preservesWireNameWithJsonName() { + Model m = inputModel("Kinesis", "body"); + 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"); + assertFalse(renamed.hasTrait(XmlNameTrait.class)); } @Test - void reservedMemberRename_Headers_alwaysRenamed() { - Optional result = GlobalTransforms.getReservedMemberRename("Headers", "apigateway"); - assertEquals(Optional.of("headerValues"), result); + void reservedRename_queryXmlService_preservesWireNameWithXmlName() { + Model m = inputModelWithProtocol("Kinesis", + new software.amazon.smithy.aws.traits.protocols.AwsQueryTrait(), "body"); + 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"); + 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 reservedMemberRename_Headers_alwaysRenamed_anyService() { - Optional result = GlobalTransforms.getReservedMemberRename("Headers", "kinesis"); - assertEquals(Optional.of("headerValues"), result); + 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 = 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(), + "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)); } - @Test - void reservedMemberRename_normalMember_returnsEmpty() { - Optional result = GlobalTransforms.getReservedMemberRename("name", "kinesis"); - assertTrue(result.isEmpty()); + /** 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 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() @@ -139,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() @@ -184,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() @@ -224,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() @@ -277,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() @@ -334,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() @@ -374,13 +441,72 @@ void computeReachableShapes_errorShapes_areReachable() { assertTrue(reachable.contains(ShapeId.from("com.example#MyError"))); } + @Test + void computeReachableShapes_excludesStructReachableOnlyViaDeprecatedOperation() { + // 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() + .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 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() @@ -454,14 +580,100 @@ void dropDeprecatedMembers_orphanedTargetBecomesUnreachable() { "shape referenced only via a deprecated member must become unreachable"); } + @Test + void dropDeprecatedMembers_sharedTargetSurvivesViaNonDeprecatedReference() { + // 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() + .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 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") + .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 +682,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(); } @@ -483,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); @@ -497,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(); @@ -510,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); @@ -522,11 +738,104 @@ 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); 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 = new GlobalTransforms().transform(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 = new GlobalTransforms().transform(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_skipsDeprecatedOperationOutputSharedByLiveOp() { + // 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() + .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. + Model model = oneOutputModel( + software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait.builder().build()); + Model out = new GlobalTransforms().transform(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/MemberRendererOutputTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/MemberRendererOutputTest.java index 8eafddf2c4d5..cfc961f11685 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.*; @@ -39,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 ")); @@ -49,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(); @@ -73,11 +70,49 @@ 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 - // 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") @@ -102,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); @@ -115,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); @@ -132,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 7bd57f1e0ccb..354199013f59 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;"); @@ -394,4 +387,75 @@ 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, so it keeps its getter 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/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 000000000000..2f147eb11c00 --- /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 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; +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 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} + * 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"; + // 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;"; + + /** + * 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(); + 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 the transform+classifier 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(); + 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(smithyServiceName); + ServiceShape service = model.expectShape( + ShapeId.from("com.amazonaws.dynamodb#DynamoDB_20120810"), ServiceShape.class); + // 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) + .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 (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); + + // 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: 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") + .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); + } +} 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 483d2f652a20..36373f8b21b6 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 01834ac14ae4..727ddcd34827 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,9 +69,8 @@ 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. - StructureShape input = StructureShape.builder() + // Input carries plain + httpHeader + httpQuery members so both request predicates fire. + StructureShape.Builder inputBuilder = StructureShape.builder() .id("com.example#DoThingInput") .addMember("name", str.getId()) .addMember("nested", nested.getId()) @@ -83,8 +79,8 @@ 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()); + StructureShape input = inputBuilder.build(); // Output carries a plain member and an httpResponseCode member. StructureShape output = StructureShape.builder() .id("com.example#DoThingOutput") @@ -93,11 +89,17 @@ 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 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()) - .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") @@ -125,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); @@ -391,12 +393,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 @@ -488,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); } @@ -506,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 fa3614153182..4622ddb31d8e 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,9 +8,13 @@ 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 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; +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 +23,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; @@ -88,6 +94,119 @@ 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); + } + + private static Model supportsPresigningModel(boolean marked) { + StringShape str = StringShape.builder().id("com.example#String").build(); + 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) { + opB.addTrait(new SupportsPresigningTrait()); + } + 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(); + } + + /** 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() + .id("com.example#DoThingOutput").addMember("result", str.getId()).build(); + OperationShape op = OperationShape.builder().id("com.example#DoThing") + .output(output.getId()) + .addTrait(new SupportsPresigningTrait()) + .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 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(); + } + + @Test + void supportsPresigningTrait_emitsProtectedDumpBodyToUrlOverride() { + // C2J's RequestHeader.vm emits DumpBodyToUrl protocol-agnostically under + // #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( + "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 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)); + assertFalse(h.contains("DumpBodyToUrl"), + "unmarked requests must not declare DumpBodyToUrl: " + h); + } + @Test void streamingResponseRequest_hasEventStreamAugmentation() { // Model: operation with streaming OUTPUT only (like SubscribeToShard / ConverseStream) @@ -105,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;"); @@ -128,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 {}; }"), @@ -157,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); } @@ -200,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() { @@ -263,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); @@ -304,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 = @@ -353,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 = @@ -451,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(); @@ -504,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"); @@ -559,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); @@ -575,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( @@ -648,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"), @@ -661,4 +750,330 @@ 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 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); + 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); + } + } + + // --- aws.auth#unsignedPayload (SignBody) --- + + /** 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"); + 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); + } + + // --- 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); + } + + // --- 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 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); + } + + @Test + void longPollingTrait_emittedWithTopIdentityMethods() { + // Ordering: IsLongPollingOperation sits with the top identity methods (after + // GetServiceRequestName, before SerializePayload), not the SignBody/IsChunked block. + 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/ResultRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ResultRendererTest.java index eeea14d4920b..c543526c953e 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,11 +57,42 @@ 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 - // 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); } @@ -272,4 +303,64 @@ 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 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(); + 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/ServiceNameUtilTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ServiceNameUtilTest.java index ecbf42c20936..9d8e5ba74de0 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 7c709a1e0621..58526386d4a0 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.*; @@ -423,6 +424,287 @@ void noInputOperation_stillProducesRequest() { "Expected a RequestInfo for the no-input operation Ping"); } + /** 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. + 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()); + } + + /** @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(); + // 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()); + } + + /** 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() + .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()); + } + + /** @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() + .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 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)); + assertFalse(classified.subObjects().stream() + .anyMatch(s -> s.getId().getName().equals("SelectEventStream")), + "incoming event-stream union dropped from subObjects: " + classified.subObjects()); + } + + @Test + void deprecatedOperation_inputAndOutputExcludedFromRequestsAndResults() { + // 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(); + 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 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 7f57785b4419..000000000000 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeUtilExtensionsTest.java +++ /dev/null @@ -1,65 +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_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() { - 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")); - } -} 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 9052f7abd91f..891480b256ba 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,24 +20,19 @@ 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; /** - * 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() @@ -57,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() @@ -91,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(); @@ -118,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); @@ -138,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 "), @@ -151,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:"), @@ -168,4 +159,76 @@ 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 --- + + /** {@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(); + // 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 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"), + "Query dual-role output must not receive the requestId stamp: " + h); + } } 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 c178d0abb85e..3e006c7ea4e9 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); @@ -54,17 +69,16 @@ 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(); - 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); @@ -72,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/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 258c1d41361d..4a1cea49663c 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 7f4281bf8f2a..1e4755a77638 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(); @@ -175,10 +181,88 @@ 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_additionalHeadersTrait_emitsConstantHeaderBeforeMemberHeaders() { + // 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"))) + .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); + 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 + void supportsPresigning_emitsDumpBodyToUrlStubImpl() { + // 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)); + 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 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 d0b31058ebad..42fe60411dc1 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,21 @@ void queryXml_subobjectHeader_hasStreamFwd() { q.serdeIncludes(FileKind.SUBOBJECT_HEADER).toString()); } + @Test + void requestSource_everyProtocolIncludesNumericForListHeaderAccumulate() { + // 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), + 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/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 39a7858d7bdd..ae96eb2b59e8 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 76be69124d09..5ae6596afae9 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 dc04e36ef508..a9778cae4857 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(); @@ -58,6 +56,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(); @@ -235,30 +239,78 @@ 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)); + // 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 + 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 - void queryXml_serializePayloadAndProtectedDumpBodyToUrl() { + void queryXml_serializePayloadAndDumpBodyToUrlImpl() { + // 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, opDoThing(), model)); + String d = renderInClassBody(w -> q.writeRequestMethodDecls(w, "AWS_EX_API", req, opDoThingPresigning(), 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)); + 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_withHeaderMember_alsoEmitsHeaders_andStillDumpBodyToUrl() { + 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); 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/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 000000000000..f60dbf50a208 --- /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"); + } +} 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 000000000000..e77a05d3b1d1 --- /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 000000000000..5d5b53fa0b55 --- /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/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 000000000000..d0150f6d6192 --- /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,157 @@ +/** + * 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() { + // 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(); + + 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:")); + } +} 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 000000000000..2b5e91b311a5 --- /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,88 @@ +/** + * 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 = new AccessAnalyzerTransforms().transform(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); + assertFalse(new AccessAnalyzerTransforms().shouldRun(service(m))); + } + + @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, + () -> 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 new file mode 100644 index 000000000000..42e4382fad06 --- /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,79 @@ +/** + * 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 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 = new ApiGatewayTransforms().transform(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()); + 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); + assertTrue(a.getMember("requestBody").isPresent()); + assertTrue(a.getMember("requestHeaders").isPresent()); + } + + @Test + void noOpForOtherService() { + Model m = apiGatewayModel("SomeOther"); + 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 new file mode 100644 index 000000000000..21a8c62aac9d --- /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,65 @@ +/** + * 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 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 = 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); + assertTrue(r.getMember("Body").isEmpty(), name); + assertEquals("Body", r.getMember("requestBody").orElseThrow() + .expectTrait(JsonNameTrait.class).getValue(), name + " wire key preserved"); + } + } + + @Test + void noOpForOtherService() { + Model m = model("SomeOther"); + 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 new file mode 100644 index 000000000000..035f70ca2404 --- /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 = new ChunkedEncodingTransform().transform(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 = 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"); + } + + @Test + void mediaStoreDataSignedStreamingOp_notStamped() { + Model m = oneOpModel("MediaStore Data", "PutObject", true, false); + 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"); + } + + @Test + void s3WriteGetObjectResponseUnsignedStreamingOp_stampsInput() { + Model m = oneOpModel("S3", "WriteGetObjectResponse", true, true); + Model out = new ChunkedEncodingTransform().transform(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 = 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 new file mode 100644 index 000000000000..4d73c5079997 --- /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,84 @@ +/** + * 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} 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 { + + 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 = new DynamoDbTransforms().transform(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) does not run. + Model m = model("Kinesis", true); + assertFalse(new DynamoDbTransforms().shouldRun(service(m)), + "transform must not run for non-dynamodb services"); + } + + @Test + void noOpWhenAttributeValueAbsent() { + Model m = model("DynamoDB", false); + 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 new file mode 100644 index 000000000000..e3baad7b016f --- /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,157 @@ +/** + * 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 = new Ec2Transforms().transform(m, service(m)); + assertTrue(EnumRenderer.getEnumValues( + out.expectShape(ShapeId.from("com.example#SpotInstanceState"))).contains("disabled")); + } + + @Test + void noOpForOtherService() { + Model m = ec2Model("SomeOther"); + assertFalse(new Ec2Transforms().shouldRun(service(m))); + } + + @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 = new Ec2Transforms().transform(m, service); + + assertFalse(out.getShape(ShapeId.from("com.example#MetricDataResult")).isPresent()); + assertTrue(out.getShape(ShapeId.from("com.example#MetricDataResponse")).isPresent()); + } + + @Test + 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, + () -> new Ec2Transforms().transform(m, service(m))); + } + + /** + * 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 = 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). + 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"); + + 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 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 = new Ec2Transforms().transform(userDataModel(), service(userDataModel())); + assertThrows(IllegalStateException.class, + () -> 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 new file mode 100644 index 000000000000..f85f64e97683 --- /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,160 @@ +/** + * 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(); + assertFalse(new GlacierTransforms().shouldRun(svc), "non-glacier service must not run"); + } + + @Test + void stampsVersionHeaderOnStreamingRequestInputs() { + StructureShape upload = streamingInput("UploadArchiveInput"); + OperationShape uploadOp = op("UploadArchive", upload); + ServiceShape svc = glacierService("Glacier", uploadOp); + Model out = new GlacierTransforms().transform(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 = new GlacierTransforms().transform(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 = new GlacierTransforms().transform(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 = new GlacierTransforms().transform(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 = 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), + "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/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 000000000000..81034fc2313b --- /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,59 @@ +/** + * 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 = new LambdaTransforms().transform(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()); + 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"); + 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 new file mode 100644 index 000000000000..aba0216fbf12 --- /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,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 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 = new LongPollingTransform().transform(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 = new LongPollingTransform().transform(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 = new LongPollingTransform().transform(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"); + 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 new file mode 100644 index 000000000000..b5678579ceea --- /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,47 @@ +/** + * 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 = new S3ControlTransforms().transform(m, service(m)); + assertTrue(out.expectShape(ShapeId.from(NS + "#CreateAccessPointResult")) + .hasTrait(TopLevelHostIdTrait.class), "result shape marked"); + } + + @Test + void noOpForOtherService() { + Model m = model("SomethingElse"); + 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 new file mode 100644 index 000000000000..b4d560f86ecf --- /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,480 @@ +/** + * 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.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.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.*; + +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(); + assertFalse(new S3Transforms().shouldRun(svc), "non-s3 service must not run"); + } + + @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 = new S3Transforms().transform(m, svc); + assertNotNull(out); + 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 renamesCopyObjectResultShapeAndMember() { + ServiceShape svc = s3RestXmlService("S3"); + StructureShape copyResult = StructureShape.builder().id(NS + "#CopyObjectResult") + .addMember("ETag", ShapeId.from("smithy.api#String")).build(); + StructureShape copyOutput = StructureShape.builder().id(NS + "#CopyObjectOutput") + .addMember("CopyObjectResult", copyResult.getId()).build(); + Model m = modelWith(svc, copyResult, copyOutput); + Model out = new S3Transforms().transform(m, svc); + + assertTrue(out.getShape(ShapeId.from(NS + "#CopyObjectResultDetails")).isPresent(), + "shape renamed to CopyObjectResultDetails"); + assertFalse(out.getShape(ShapeId.from(NS + "#CopyObjectResult")).isPresent(), + "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 + 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, () -> new S3Transforms().transform(m, svc)); + } + + /** 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") + .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(); + 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 = 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); + 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); + } + + /** + * 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(); + 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 = 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, + "NextPartNumberMarker retyped to integer to preserve the shipped C2J int API"); + 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 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 = 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), + "PutBucketPolicyRequest is in REQUESTS_TO_OVERRIDE_STREAMING"); + assertFalse(out.expectShape(other.getId()).hasTrait(OverrideStreamingTrait.class), + "other requests are untouched"); + } + + /** + * 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(); + 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 = 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()); + 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 = 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"); + } + + @Test + void addsExpiresStringToOutputAndDeprecatesExpires() { + Model m = expiresModel(); + Model out = new S3Transforms().transform(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(HttpHeaderTrait.class).getValue(), + "ExpiresString reads the same Expires header"); + MemberShape expiresMember = outShape.getMember("Expires").orElseThrow(); + 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 = new S3Transforms().transform(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"); + } + + @Test + void appendsMissingBucketLocationConstraintRegions() { + 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 out = new S3Transforms().transform(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"); + // 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 + 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 = 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); + long usEast1 = result.getEnumValues().values().stream().filter("us-east-1"::equals).count(); + 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 = new S3Transforms().transform(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 injectsGetObjectId2Only() { + 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 = new S3Transforms().transform(m, svc); + + assertTrue(out.getShape(ShapeId.from(NS + "#ObjectId2")).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()); + + // 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"); + } + + /** 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")) + .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 = new S3Transforms().transform(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()); + + // 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"); + + 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 stampsCustomizedAccessLogTagMarkerAndKeepsQueryParams() { + Model m = accessLogModel(); + Model out = new S3Transforms().transform(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(); + Model out = new S3Transforms().transform(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 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 = new S3Transforms().transform(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(); + 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() + .filter("customizedAccessLogTag"::equals).count(); + assertEquals(1, count, "re-applying must not duplicate the injected member"); + } +} 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 000000000000..4b20eafdac28 --- /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,98 @@ +/** + * 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 { + + // 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 = new SourceRegionTransform().transform(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() { + Model m = modelWithOp("RDS", "DescribeDBClusters", "DescribeDBClustersRequest"); + Model out = new SourceRegionTransform().transform(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"); + assertFalse(new SourceRegionTransform().shouldRun(service(m))); + } + + @Test + void idempotent_doesNotDuplicateExistingMember() { + Model m = modelWithOp("RDS", "CopyDBClusterSnapshot", "CopyDBClusterSnapshotRequest"); + 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(); + assertEquals(1, count); + } + + @Test + void injectsSourceRegionIntoDocDbRequest() { + Model m = modelWithOp("DocDB", "CreateDBCluster", "CreateDBClusterMessage"); + Model out = new SourceRegionTransform().transform(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 = 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 new file mode 100644 index 000000000000..91eec74c1602 --- /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 = new SqsTransforms().transform(m, svc); + List values = EnumRenderer.getEnumValues( + out.expectShape(ShapeId.from("com.example#QueueAttributeName"))); + assertTrue(values.containsAll(ADDED)); + assertTrue(values.contains("All")); + } + + @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 = new SqsTransforms().transform(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 = 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(); + assertEquals(1, senderId); + } +} 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 000000000000..78960b1adcc1 --- /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,121 @@ +/** + * 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); + } + + /** 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); + } + + /** 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(); + } + + /** + * 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(); + 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_stampsEveryOperation() { + Model m = twoOpModel(new AwsQueryTrait(), null, "GetUser", "CreateUser"); + 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"); + } + + @Test + void ec2Service_stampsEveryOperation() { + Model m = twoOpModel(new Ec2QueryTrait(), null, "DescribeThings", "RunThings"); + 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"); + } + + @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 = 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"); + } + + @Test + void pollyService_stampsOnlySynthesizeSpeechOperation() { + Model m = twoOpModel(RestJson1Trait.builder().build(), pollyServiceTrait(), + "SynthesizeSpeech", "DescribeVoices"); + 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"); + } + + @Test + void plainRestJsonService_stampsNothing() { + Model m = twoOpModel(RestJson1Trait.builder().build(), null, "GetThing", "PutThing"); + 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")); + } +} 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 000000000000..86f54fbb4de6 --- /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,233 @@ +/** + * 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.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; +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.*; + +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", Protocol.JSON) + .isEmpty()); + } + + @Test + void renameMember_targetExists_throws() { + StructureShape s = struct("body", "requestBody"); + assertThrows(IllegalStateException.class, + () -> 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", Protocol.JSON) + .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_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)); + } + + 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 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") + .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"); + } +}