From 07dd0c34848b74692966523eea6fcf999576d3cc Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 20 Jul 2026 16:50:08 -0400 Subject: [PATCH 01/18] Add result and request specific generation --- .../generators/model/MemberRenderer.java | 206 +++++++- .../generators/model/ModelGenerator.java | 496 +++++++++++++++--- .../generators/model/ShapeClassifier.java | 32 +- .../generators/model/ShapeClassifierTest.java | 8 +- 4 files changed, 634 insertions(+), 108 deletions(-) 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 0d8a6ca1238..d2b64a0d0ab 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 @@ -37,7 +37,7 @@ private MemberRenderer() { *
  • HasBeenSet check
  • *
  • Templated Set method
  • *
  • Templated With method (fluent)
  • - *
  • Templated Add method (list members only)
  • + *
  • Templated Add method (list/map members only)
  • * * * @param writer the CppWriter to write to @@ -63,12 +63,7 @@ public static void renderPublicSection(CppWriter writer, StructureShape shape, // Documentation comment if (member.getTrait(DocumentationTrait.class).isPresent()) { - String doc = member.getTrait(DocumentationTrait.class).get().getValue(); - writer.write("/**"); - for (String line : sanitizeDoc(doc).split("\n")) { - writer.write(" $L", "* " + line); - } - writer.write(" */"); + writeDocComment(writer, collapseWhitespace(member.getTrait(DocumentationTrait.class).get().getValue())); } else { writer.write(""); } @@ -115,7 +110,6 @@ public static void renderPublicSection(CppWriter writer, StructureShape shape, targetShape.asListShape().get().getMember().getTarget()); String elementType = CppTypeMapper.getCppType(elementShape, model); if (elementShape.isEnumShape()) { - // Enum elements use non-templated value Add writer.openBlock("inline $L& Add$L($L value) {", "}", className, memberName, elementType, () -> { writer.write("$LHasBeenSet = true;", fieldName); writer.write("$L.push_back(value);", fieldName); @@ -130,6 +124,24 @@ public static void renderPublicSection(CppWriter writer, StructureShape shape, }); } } + + // Add method for map types (two template params: key + value) + if (targetShape.isMapShape()) { + Shape keyShape = model.expectShape( + targetShape.asMapShape().get().getKey().getTarget()); + Shape valueShape = model.expectShape( + targetShape.asMapShape().get().getValue().getTarget()); + String keyType = CppTypeMapper.getCppType(keyShape, model); + String valueType = CppTypeMapper.getCppType(valueShape, model); + String keyParam = memberName + "KeyT"; + String valueParam = memberName + "ValueT"; + writer.write("template ", keyParam, keyType, valueParam, valueType); + writer.openBlock("$L& Add$L($L&& key, $L&& value) {", "}", className, memberName, keyParam, valueParam, () -> { + writer.write("$LHasBeenSet = true;", fieldName); + writer.write("$L.emplace(std::forward<$L>(key), std::forward<$L>(value));", fieldName, keyParam, valueParam); + writer.write("return *this;"); + }); + } } writer.write("///@}"); @@ -140,6 +152,111 @@ public static void renderPublicSection(CppWriter writer, StructureShape shape, } } + /** + * Writes public accessor methods for result shapes (no HasBeenSet methods). + * + * @param writer the CppWriter to write to + * @param shape the structure shape whose members to render + * @param model the model (for resolving member targets) + * @param exportMacro the export macro (e.g., "AWS_KINESIS_API") + * @param className the C++ class name (e.g., "GetItemResult") + */ + public static void renderPublicSectionForResult(CppWriter writer, StructureShape shape, + Model model, String exportMacro, String className) { + java.util.List> members = + new java.util.ArrayList<>(shape.getAllMembers().entrySet()); + for (int i = 0; i < members.size(); i++) { + Map.Entry entry = members.get(i); + String memberName = entry.getKey(); + MemberShape member = entry.getValue(); + Shape targetShape = model.expectShape(member.getTarget()); + String cppType = CppTypeMapper.getCppType(targetShape, model); + String fieldName = "m_" + decapitalize(memberName); + String templateParam = memberName + "T"; + + writer.write("///@{"); + + if (member.getTrait(DocumentationTrait.class).isPresent()) { + writeDocComment(writer, collapseWhitespace(member.getTrait(DocumentationTrait.class).get().getValue())); + } else { + writer.write(""); + } + + // Getter + if (isPrimitive(targetShape) || targetShape.isEnumShape()) { + writer.write("inline $L Get$L() const { return $L; }", cppType, memberName, fieldName); + } else { + writer.write("inline const $L& Get$L() const { return $L; }", cppType, memberName, fieldName); + } + + // No HasBeenSet for results + + if (targetShape.isEnumShape() || isPrimitive(targetShape)) { + writer.openBlock("inline void Set$L($L value) {", "}", memberName, cppType, () -> { + writer.write("$LHasBeenSet = true;", fieldName); + writer.write("$L = value;", fieldName); + }); + writer.openBlock("inline $L& With$L($L value) {", "}", className, memberName, cppType, () -> { + writer.write("Set$L(value);", memberName); + writer.write("return *this;"); + }); + } else { + writer.write("template ", templateParam, cppType); + writer.openBlock("void Set$L($L&& value) {", "}", memberName, templateParam, () -> { + writer.write("$LHasBeenSet = true;", fieldName); + writer.write("$L = std::forward<$L>(value);", fieldName, templateParam); + }); + writer.write("template ", templateParam, cppType); + writer.openBlock("$L& With$L($L&& value) {", "}", className, memberName, templateParam, () -> { + writer.write("Set$L(std::forward<$L>(value));", memberName, templateParam); + writer.write("return *this;"); + }); + + if (targetShape.isListShape()) { + Shape elementShape = model.expectShape( + targetShape.asListShape().get().getMember().getTarget()); + String elementType = CppTypeMapper.getCppType(elementShape, model); + if (elementShape.isEnumShape()) { + writer.openBlock("inline $L& Add$L($L value) {", "}", className, memberName, elementType, () -> { + writer.write("$LHasBeenSet = true;", fieldName); + writer.write("$L.push_back(value);", fieldName); + writer.write("return *this;"); + }); + } else { + writer.write("template ", templateParam, elementType); + writer.openBlock("$L& Add$L($L&& value) {", "}", className, memberName, templateParam, () -> { + writer.write("$LHasBeenSet = true;", fieldName); + writer.write("$L.emplace_back(std::forward<$L>(value));", fieldName, templateParam); + writer.write("return *this;"); + }); + } + } + + if (targetShape.isMapShape()) { + Shape keyShape = model.expectShape( + targetShape.asMapShape().get().getKey().getTarget()); + Shape valueShape = model.expectShape( + targetShape.asMapShape().get().getValue().getTarget()); + String keyType = CppTypeMapper.getCppType(keyShape, model); + String valueType = CppTypeMapper.getCppType(valueShape, model); + String keyParam = memberName + "KeyT"; + String valueParam = memberName + "ValueT"; + writer.write("template ", keyParam, keyType, valueParam, valueType); + writer.openBlock("$L& Add$L($L&& key, $L&& value) {", "}", className, memberName, keyParam, valueParam, () -> { + writer.write("$LHasBeenSet = true;", fieldName); + writer.write("$L.emplace(std::forward<$L>(key), std::forward<$L>(value));", fieldName, keyParam, valueParam); + writer.write("return *this;"); + }); + } + } + + writer.write("///@}"); + if (i < members.size() - 1) { + writer.write(""); + } + } + } + /** * Writes the private member fields and HasBeenSet flags for a structure. * @@ -154,7 +271,14 @@ public static void renderPublicSection(CppWriter writer, StructureShape shape, * @param model the model (for resolving member targets) */ public static void renderPrivateSection(CppWriter writer, StructureShape shape, Model model) { - // First: data members with blank lines between each (except after the last) + renderPrivateDataMembers(writer, shape, model); + renderPrivateHasBeenSetFlags(writer, shape, model); + } + + /** + * Writes only the data member declarations (with blank lines between each). + */ + public static void renderPrivateDataMembers(CppWriter writer, StructureShape shape, Model model) { java.util.List> entries = new java.util.ArrayList<>(shape.getAllMembers().entrySet()); for (int i = 0; i < entries.size(); i++) { @@ -169,13 +293,16 @@ public static void renderPrivateSection(CppWriter writer, StructureShape shape, defaultVal -> writer.write("$L $L{$L};", cppType, fieldName, defaultVal), () -> writer.write("$L $L;", cppType, fieldName) ); - // Blank line between data members, but not after the last one if (i < entries.size() - 1) { writer.write(""); } } + } - // Then: hasBeenSet flags grouped together (no leading blank line) + /** + * Writes only the HasBeenSet boolean flags (one per member, no blank lines between). + */ + public static void renderPrivateHasBeenSetFlags(CppWriter writer, StructureShape shape, Model model) { for (Map.Entry entry : shape.getAllMembers().entrySet()) { String memberName = entry.getKey(); String fieldName = "m_" + decapitalize(memberName); @@ -193,7 +320,60 @@ private static String decapitalize(String name) { return Character.toLowerCase(name.charAt(0)) + name.substring(1); } - private static String sanitizeDoc(String doc) { - return doc.replace("\n", "\n * "); + private static final String[] UNSUPPORTED_HTML_TAGS = { + "", "", "", "" + }; + + static void writeDocComment(CppWriter writer, String doc) { + writer.write("/**"); + String formatted = formatDocumentation(doc); + for (String line : formatted.split("\n")) { + writer.write(" * $L", line); + } + writer.write(" */"); + } + + static String collapseWhitespace(String text) { + if (text == null) { + return null; + } + return text.replaceAll("\\s+", " ").trim(); + } + + static String formatDocumentation(String documentation) { + if (documentation == null) { + return ""; + } + String text = documentation.replace("/*", "/ *").replace("*/", "* /"); + for (String tag : UNSUPPORTED_HTML_TAGS) { + text = text.replace(tag, ""); + } + return wrapText(text, 80); + } + + private static String wrapText(String text, int wrapLength) { + if (text == null || text.length() <= wrapLength) { + return text; + } + StringBuilder result = new StringBuilder(); + int offset = 0; + while (offset < text.length()) { + if (text.length() - offset <= wrapLength) { + result.append(text, offset, text.length()); + break; + } + int spaceToWrapAt = text.lastIndexOf(' ', offset + wrapLength); + if (spaceToWrapAt <= offset) { + spaceToWrapAt = text.indexOf(' ', offset + wrapLength); + if (spaceToWrapAt < 0) { + result.append(text, offset, text.length()); + break; + } + } + result.append(text, offset, spaceToWrapAt); + result.append('\n'); + offset = spaceToWrapAt + 1; + } + return result.toString(); } } 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 2cbde7ae331..9a74fc74689 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java @@ -8,14 +8,21 @@ 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.ShapeClassifier.RequestInfo; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.ResultInfo; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.aws.traits.clientendpointdiscovery.ClientEndpointDiscoveryTrait; +import software.amazon.smithy.rulesengine.traits.ContextParamTrait; +import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -60,48 +67,24 @@ public void generateAll() { // Generate sub-object headers + sources for (Shape shape : classified.subObjects()) { if (shape.isStructureShape()) { - generateStructureHeader(shape.asStructureShape().get(), null); - generateStructureSource(shape.asStructureShape().get(), null); + generateSubObjectHeader(shape.asStructureShape().get()); + generateSubObjectSource(shape.asStructureShape().get()); } } // Generate request headers + sources (Input -> Request) - for (StructureShape request : classified.requests()) { - String cppName = mapRequestName(request.getId().getName()); - generateStructureHeader(request, cppName); - generateStructureSource(request, cppName); + for (RequestInfo reqInfo : classified.requests()) { + generateRequestHeader(reqInfo.shape(), reqInfo.operation()); + generateRequestSource(reqInfo.shape(), reqInfo.operation()); } // Generate result headers + sources (Output -> Result) - for (StructureShape result : classified.results()) { - String cppName = mapResultName(result.getId().getName()); - generateStructureHeader(result, cppName); - generateStructureSource(result, cppName); + for (ResultInfo resInfo : classified.results()) { + generateResultHeader(resInfo.shape(), resInfo.operation()); + generateResultSource(resInfo.shape(), resInfo.operation()); } } - /** - * Maps a Smithy input shape name to the C++ Request class name. - * e.g., "AddTagsToStreamInput" -> "AddTagsToStreamRequest" - */ - private String mapRequestName(String smithyName) { - if (smithyName.endsWith("Input")) { - return smithyName.substring(0, smithyName.length() - "Input".length()) + "Request"; - } - return smithyName; - } - - /** - * Maps a Smithy output shape name to the C++ Result class name. - * e.g., "DescribeLimitsOutput" -> "DescribeLimitsResult" - */ - private String mapResultName(String smithyName) { - if (smithyName.endsWith("Output")) { - return smithyName.substring(0, smithyName.length() - "Output".length()) + "Result"; - } - return smithyName; - } - private void generateEnumHeader(Shape enumShape) { String fileName = "include/aws/" + smithyServiceName + "/model/" + enumShape.getId().getName() + ".h"; writerDelegator.useFileWriter(fileName, writer -> @@ -114,22 +97,17 @@ private void generateEnumSource(Shape enumShape) { EnumRenderer.renderSource(writer, enumShape, namespace, exportMacro, smithyServiceName)); } - private void generateStructureHeader(StructureShape shape, String classNameOverride) { - String className = classNameOverride != null ? classNameOverride : shape.getId().getName(); + private void generateSubObjectHeader(StructureShape shape) { + String className = shape.getId().getName(); String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; writerDelegator.useFileWriter(fileName, writer -> { - // Copyright + pragma - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); + writeCopyright(writer); writer.write("#pragma once"); // Includes renderIncludes(writer, shape); - // Include unless ALL members are primitives (matches C2J behavior) + // Include unless ALL members are primitives boolean allPrimitive = shape.getAllMembers().values().stream() .map(m -> model.expectShape(m.getTarget())) .allMatch(ModelGenerator::isPrimitive); @@ -139,13 +117,13 @@ private void generateStructureHeader(StructureShape shape, String classNameOverr } writer.write(""); - // Open namespace Aws (contains both forward decls and service namespace) + // Open namespace Aws writer.writeNamespaceOpen("Aws"); // Forward declarations for serde types renderForwardDeclarations(writer); - // Service namespace (still inside namespace Aws) + // Service namespace writer.writeNamespaceOpen(namespace); writer.writeNamespaceOpen("Model"); writer.write(""); @@ -161,7 +139,7 @@ private void generateStructureHeader(StructureShape shape, String classNameOverr SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className); writer.write(""); - // Member accessors + // Member accessors (with HasBeenSet) MemberRenderer.renderPublicSection(writer, shape, model, exportMacro, className); // Private section @@ -179,46 +157,236 @@ private void generateStructureHeader(StructureShape shape, String classNameOverr }); } - private void generateStructureSource(StructureShape shape, String classNameOverride) { - String className = classNameOverride != null ? classNameOverride : shape.getId().getName(); + private void generateSubObjectSource(StructureShape shape) { + String className = shape.getId().getName(); String fileName = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(fileName, writer -> { - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); + writeCopyright(writer); // Includes - if (protocol.isJsonLike()) { - writer.write("#include "); - } else { - writer.write("#include "); - } - writer.write("#include ", smithyServiceName, className); + writeSourceIncludes(writer, className, false, false); + + // Explicit namespace blocks for sub-objects + writer.writeNamespaceOpen("Aws"); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); writer.write(""); - writer.write("#include "); + + // Serde implementation + SerdeStub.renderSourceImplementation(writer, shape, model, protocol, + className, serviceName, smithyServiceName); writer.write(""); - // Using declarations - if (protocol.isJsonLike()) { - writer.write("using namespace Aws::Utils::Json;"); - writer.write("using namespace Aws::Utils;"); - } else { - writer.write("using namespace Aws::Utils::Xml;"); - writer.write("using namespace Aws::Utils;"); + // Namespace close + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + private void generateRequestHeader(StructureShape shape, OperationShape operation) { + String className = getRequestClassName(operation); + String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + + // Includes - request needs the service request base class + Set includes = new TreeSet<>(); + includes.add(""); + includes.add(""); + List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); + includes.addAll(memberIncludes); + for (String include : includes) { + writer.write("#include $L", include); + } + + // Include unless ALL members are primitives + boolean allPrimitive = shape.getAllMembers().values().stream() + .map(m -> model.expectShape(m.getTarget())) + .allMatch(ModelGenerator::isPrimitive); + if (!allPrimitive) { + writer.write(""); + writer.write("#include "); } writer.write(""); - // Namespace open + // Open namespace Aws - no forward declarations for requests (base class provides them) writer.writeNamespaceOpen("Aws"); writer.writeNamespaceOpen(namespace); writer.writeNamespaceOpen("Model"); writer.write(""); - // Serde implementation - SerdeStub.renderSourceImplementation(writer, shape, model, protocol, - className, serviceName, smithyServiceName); + // Class-level doc comment + renderClassDocComment(writer, shape); + + // Class declaration - inherits from service request + String baseClass = namespace + "Request"; + writer.openBlock("class $L : public $L {", "};", className, baseClass, () -> { + writer.write("public:"); + + // Export macro + default constructor + writer.write("$L $L() = default;", exportMacro, className); + writer.write(""); + + // Declare service-specific overrides + writer.write("// Service request name is the Operation name which will send this request out,"); + writer.write("// each operation should has unique request name, so that we can get operation's name from this request."); + writer.write("// Note: this is not true for response, multiple operations may have the same response name,"); + writer.write("// so we can not get operation's name from response."); + writer.write("inline virtual const char* GetServiceRequestName() const override { return \"$L\"; }", operation.getId().getName()); + writer.write(""); + writer.write("$L Aws::String SerializePayload() const override;", exportMacro); + writer.write(""); + writer.write("$L Aws::Http::HeaderValueCollection GetRequestSpecificHeaders() const override;", exportMacro); + + // Endpoint context params if needed + if (hasEndpointContextParams(operation, shape)) { + writer.write(""); + writer.write("/**"); + writer.write(" * Helper function to collect parameters (configurable and static hardcoded) required for endpoint computation."); + writer.write(" */"); + writer.write("$L EndpointParameters GetEndpointContextParams() const override;", exportMacro); + } + + writer.write(""); + + // Member accessors (with HasBeenSet) + MemberRenderer.renderPublicSection(writer, shape, model, exportMacro, className); + + // Private section - only emit if there are members + if (!shape.getAllMembers().isEmpty()) { + writer.dedent(); + writer.write("private:"); + writer.indent(); + MemberRenderer.renderPrivateSection(writer, shape, model); + } + }); + writer.write(""); + + // Namespace close + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + private void generateRequestSource(StructureShape shape, OperationShape operation) { + String className = getRequestClassName(operation); + String fileName = "source/model/" + className + ".cpp"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + + // Includes and using namespace (not explicit namespace blocks) + writeSourceIncludes(writer, className, true, false); + + // SerializePayload stub + writer.write("Aws::String $L::SerializePayload() const { return \"{}\"; }", className); + writer.write(""); + + // GetRequestSpecificHeaders stub + writer.write("Aws::Http::HeaderValueCollection $L::GetRequestSpecificHeaders() const { return {}; }", className); + + // GetEndpointContextParams if needed + if (hasEndpointContextParams(operation, shape)) { + writer.write(""); + renderEndpointContextParams(writer, className, operation, shape); + } + writer.write(""); + }); + } + + private void generateResultHeader(StructureShape shape, OperationShape operation) { + String className = getResultClassName(operation); + String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + + // Includes — match C2J: HttpResponse, AWSString, service exports, member types + Set includes = new TreeSet<>(); + includes.add(""); + includes.add(""); + includes.add(""); + List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); + includes.addAll(memberIncludes); + for (String include : includes) { + writer.write("#include $L", include); + } + + // Always include for results (synthetic RequestId is Aws::String) + writer.write(""); + writer.write("#include "); + writer.write(""); + + // Open namespace Aws with forward declarations matching C2J pattern + writer.writeNamespaceOpen("Aws"); + // Forward-declare AmazonWebServiceResult template + writer.write("template "); + writer.write("class AmazonWebServiceResult;"); + writer.write(""); + // Forward-declare JsonValue only (not JsonView) + writer.writeNamespaceOpen("Utils"); + writer.writeNamespaceOpen("Json"); + writer.write("class JsonValue;"); + writer.writeNamespaceClose("Json"); + writer.writeNamespaceClose("Utils"); + + // Service namespace + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + + // Class-level doc comment — only emit if shape has documentation (C2J doesn't emit empty block for results) + if (shape.getTrait(DocumentationTrait.class).isPresent()) { + renderClassDocComment(writer, shape); + } + + // Class declaration + writer.openBlock("class $L {", "};", className, () -> { + writer.write("public:"); + + // Export macro + constructors + writer.write("$L $L() = default;", exportMacro, className); + writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + writer.write(""); + + // Member accessors (NO HasBeenSet for results) + MemberRenderer.renderPublicSectionForResult(writer, shape, model, exportMacro, className); + + // Synthetic members: RequestId + writer.write(""); + writer.write("///@{"); + writer.write(""); + writer.write("inline const Aws::String& GetRequestId() const { return m_requestId; }"); + writer.write("template "); + writer.openBlock("void SetRequestId(RequestIdT&& value) {", "}", () -> { + writer.write("m_requestIdHasBeenSet = true;"); + writer.write("m_requestId = std::forward(value);"); + }); + writer.write("template "); + writer.openBlock("$L& WithRequestId(RequestIdT&& value) {", "}", className, () -> { + writer.write("SetRequestId(std::forward(value));"); + writer.write("return *this;"); + }); + writer.write("///@}"); + + // HttpResponseCode accessor + writer.write("inline Aws::Http::HttpResponseCode GetHttpResponseCode() const { return m_HttpResponseCode; }"); + writer.write(""); + + // Private section + writer.dedent(); + writer.write("private:"); + writer.indent(); + MemberRenderer.renderPrivateDataMembers(writer, shape, model); + writer.write(""); + writer.write("Aws::String m_requestId;"); + writer.write("Aws::Http::HttpResponseCode m_HttpResponseCode;"); + // HasBeenSet flags for all members + requestId + MemberRenderer.renderPrivateHasBeenSetFlags(writer, shape, model); + writer.write("bool m_requestIdHasBeenSet = false;"); + }); writer.write(""); // Namespace close @@ -228,11 +396,100 @@ private void generateStructureSource(StructureShape shape, String classNameOverr }); } + private void generateResultSource(StructureShape shape, OperationShape operation) { + String className = getResultClassName(operation); + String fileName = "source/model/" + className + ".cpp"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + + // Result-specific includes (matching C2J) + writer.write("#include "); + writer.write("#include "); + writer.write("#include "); + writer.write("#include "); + writer.write("#include "); + writer.write("#include ", smithyServiceName, className); + writer.write(""); + writer.write("#include "); + writer.write(""); + writer.write("using namespace Aws::$L::Model;", namespace); + writer.write("using namespace Aws::Utils::Json;"); + writer.write("using namespace Aws::Utils;"); + writer.write("using namespace Aws;"); + writer.write(""); + + // AmazonWebServiceResult constructor stub + writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { + writer.write("*this = result;"); + }); + writer.write(""); + + // operator= stub + writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { + writer.write("return *this;"); + }); + writer.write(""); + }); + } + + private String getRequestClassName(OperationShape operation) { + return operation.getId().getName() + "Request"; + } + + private String getResultClassName(OperationShape operation) { + return operation.getId().getName() + "Result"; + } + + private void writeCopyright(CppWriter writer) { + writer.write("/**"); + writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + writer.write(" * SPDX-License-Identifier: Apache-2.0."); + writer.write(" */"); + writer.write(""); + } + + /** + * Writes source file includes and using declarations. + * + * @param writer the CppWriter + * @param className the class name (for the model include) + * @param useModelNamespace if true, emit "using namespace Aws::{Namespace}::Model;" instead of explicit namespace blocks + * @param useAwsNamespace if true, also emit "using namespace Aws;" (for result sources) + */ + private void writeSourceIncludes(CppWriter writer, String className, + boolean useModelNamespace, boolean useAwsNamespace) { + // Includes + if (protocol.isJsonLike()) { + writer.write("#include "); + } else { + writer.write("#include "); + } + writer.write("#include ", smithyServiceName, className); + writer.write(""); + writer.write("#include "); + writer.write(""); + + // Using declarations + if (useModelNamespace) { + writer.write("using namespace Aws::$L::Model;", namespace); + } + if (protocol.isJsonLike()) { + writer.write("using namespace Aws::Utils::Json;"); + writer.write("using namespace Aws::Utils;"); + } else { + writer.write("using namespace Aws::Utils::Xml;"); + writer.write("using namespace Aws::Utils;"); + } + if (useAwsNamespace) { + writer.write("using namespace Aws;"); + } + writer.write(""); + } + private void renderIncludes(CppWriter writer, StructureShape shape) { Set includes = new TreeSet<>(); // sorted for deterministic output includes.add(""); - // Gather includes for all member types List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); includes.addAll(memberIncludes); @@ -264,25 +521,98 @@ private static boolean isPrimitive(Shape shape) { || shape.isFloatShape(); } - private static boolean isPrimitiveOrEnum(Shape shape) { - return isPrimitive(shape) || shape.isEnumShape(); - } - + /** + * Renders the class-level documentation comment. + * If the shape has no documentation, emits an empty doc block to match C2J output. + */ private void renderClassDocComment(CppWriter writer, StructureShape shape) { - shape.getTrait(DocumentationTrait.class).ifPresent(doc -> { - String docText = doc.getValue(); - // Add "See Also" link consistent with C2J output + if (shape.getTrait(DocumentationTrait.class).isPresent()) { + String docText = MemberRenderer.collapseWhitespace( + shape.getTrait(DocumentationTrait.class).get().getValue()); String version = service.getVersion(); String seeAlso = String.format( - "

    See Also:

    AWS\n" + - "API Reference

    ", + "

    See Also:

    AWS API Reference

    ", smithyServiceName, version, shape.getId().getName()); + MemberRenderer.writeDocComment(writer, docText + seeAlso); + } else { + // Empty doc block matching C2J output for undocumented shapes writer.write("/**"); - for (String line : (docText + seeAlso).split("\n")) { - writer.write(" $L", "* " + line); - } writer.write(" */"); + } + } + + /** + * Checks if an operation has endpoint context parameters (static or member-level). + */ + private boolean hasEndpointContextParams(OperationShape operation, StructureShape requestShape) { + // Check for @staticContextParams on the operation + if (operation.hasTrait(StaticContextParamsTrait.class)) { + return true; + } + // Check for @contextParam on any member of the request shape + for (MemberShape member : requestShape.getAllMembers().values()) { + if (member.hasTrait(ContextParamTrait.class)) { + return true; + } + } + return false; + } + + /** + * Renders the GetEndpointContextParams function body. + */ + private void renderEndpointContextParams(CppWriter writer, String className, + OperationShape operation, StructureShape shape) { + writer.openBlock("$L::EndpointParameters $L::GetEndpointContextParams() const {", "}", + className, className, () -> { + writer.write("EndpointParameters parameters;"); + + // Static context params + if (operation.hasTrait(StaticContextParamsTrait.class)) { + writer.write("// Static context parameters"); + StaticContextParamsTrait staticParams = operation.expectTrait(StaticContextParamsTrait.class); + staticParams.getParameters().forEach((name, def) -> { + def.getValue().accept(new software.amazon.smithy.model.node.NodeVisitor.Default() { + @Override + protected Void getDefault(software.amazon.smithy.model.node.Node node) { + return null; + } + @Override + public Void stringNode(software.amazon.smithy.model.node.StringNode node) { + writer.write("parameters.emplace_back(Aws::String(\"$L\"), \"$L\", Aws::Endpoint::EndpointParameter::ParameterOrigin::STATIC_CONTEXT);", + name, node.getValue()); + return null; + } + @Override + public Void booleanNode(software.amazon.smithy.model.node.BooleanNode node) { + writer.write("parameters.emplace_back(Aws::String(\"$L\"), $L, Aws::Endpoint::EndpointParameter::ParameterOrigin::STATIC_CONTEXT);", + name, node.getValue()); + return null; + } + }); + }); + } + + // Operation context params (from members with @contextParam) + boolean hasContextParams = shape.getAllMembers().values().stream() + .anyMatch(m -> m.hasTrait(ContextParamTrait.class)); + if (hasContextParams) { + writer.write("// Operation context parameters"); + for (Map.Entry entry : shape.getAllMembers().entrySet()) { + MemberShape member = entry.getValue(); + if (member.hasTrait(ContextParamTrait.class)) { + ContextParamTrait contextParam = member.expectTrait(ContextParamTrait.class); + String paramName = contextParam.getName(); + String memberName = entry.getKey(); + writer.openBlock("if ($LHasBeenSet()) {", "}", memberName, () -> { + writer.write("parameters.emplace_back(Aws::String(\"$L\"), this->Get$L(), Aws::Endpoint::EndpointParameter::ParameterOrigin::OPERATION_CONTEXT);", + paramName, memberName); + }); + } + } + } + + writer.write("return parameters;"); }); } } 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 bb3ab03b0f9..cef3712dc27 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 @@ -31,6 +31,22 @@ public enum Classification { EVENT_STREAM_HANDLER, BLOB_PAYLOAD_EVENT, OUTGOING_EVENT_STREAM, SKIP } + /** + * Associates an operation input shape with its parent operation. + * + * @param shape the request structure shape + * @param operation the operation shape that uses this as input + */ + public record RequestInfo(StructureShape shape, OperationShape operation) {} + + /** + * Associates an operation output shape with its parent operation. + * + * @param shape the result structure shape + * @param operation the operation shape that uses this as output + */ + public record ResultInfo(StructureShape shape, OperationShape operation) {} + /** * Metadata about an operation whose output contains an event stream. * @@ -43,8 +59,8 @@ public record EventStreamInfo(String operationName, StructureShape requestShape, /** * The full classification result for a service. * - * @param requests operation input shapes - * @param results operation output shapes (excluding event-stream-bearing) + * @param requests operation input shapes with their operations + * @param results operation output shapes with their operations (excluding event-stream-bearing) * @param subObjects remaining StructureShape/UnionShape reachable from operations * @param enums EnumShape or StringShape with @enum trait * @param eventStreamHandlers operation + request/result shape tuples for event stream handlers @@ -52,8 +68,8 @@ public record EventStreamInfo(String operationName, StructureShape requestShape, * @param outgoingEventStreams outgoing event stream shapes (header only) */ public record ClassifiedShapes( - List requests, - List results, + List requests, + List results, List subObjects, List enums, List eventStreamHandlers, @@ -78,8 +94,8 @@ public static ClassifiedShapes classify(Model model, ServiceShape service) { Set inputShapeIds = new HashSet<>(); Set outputShapeIds = new HashSet<>(); - List requests = new ArrayList<>(); - List results = new ArrayList<>(); + List requests = new ArrayList<>(); + List results = new ArrayList<>(); List subObjects = new ArrayList<>(); List enums = new ArrayList<>(); List eventStreamHandlers = new ArrayList<>(); @@ -91,7 +107,7 @@ public static ClassifiedShapes classify(Model model, ServiceShape service) { op.getInput().ifPresent(id -> { inputShapeIds.add(id); model.getShape(id).flatMap(Shape::asStructureShape).ifPresent(s -> { - requests.add(s); + requests.add(new RequestInfo(s, op)); // Check if operation has event-stream-bearing result boolean resultHasEventStream = op.getOutput() @@ -109,7 +125,7 @@ public static ClassifiedShapes classify(Model model, ServiceShape service) { outputShapeIds.add(id); model.getShape(id).flatMap(Shape::asStructureShape).ifPresent(s -> { if (!hasEventStreamMembers(s, model)) { - results.add(s); + results.add(new ResultInfo(s, op)); } // If has event stream members, result is skipped (handler generated instead) }); 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 20f43a4dc95..dff69c02d9b 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 @@ -53,7 +53,7 @@ void classifiesRequestShape() { ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); var classified = ShapeClassifier.classify(model, service); assertTrue(classified.requests().stream() - .anyMatch(s -> s.getId().getName().equals("GetItemRequest"))); + .anyMatch(r -> r.shape().getId().getName().equals("GetItemRequest"))); } @Test @@ -62,7 +62,7 @@ void classifiesResultShape() { ServiceShape service = model.expectShape(ShapeId.from("com.example#TestService"), ServiceShape.class); var classified = ShapeClassifier.classify(model, service); assertTrue(classified.results().stream() - .anyMatch(s -> s.getId().getName().equals("GetItemResponse"))); + .anyMatch(r -> r.shape().getId().getName().equals("GetItemResponse"))); } @Test @@ -239,11 +239,11 @@ void classifiesEventStreamHandler() { // Result should NOT include the event-stream-bearing output assertTrue(classified.results().stream() - .noneMatch(s -> s.getId().getName().equals("SubscribeResponse"))); + .noneMatch(r -> r.shape().getId().getName().equals("SubscribeResponse"))); // Request should still be classified assertTrue(classified.requests().stream() - .anyMatch(s -> s.getId().getName().equals("SubscribeRequest"))); + .anyMatch(r -> r.shape().getId().getName().equals("SubscribeRequest"))); } @Test From 90c964a00deae078025751f5b2fc9c2d0cdfe651 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 21 Jul 2026 12:56:06 -0400 Subject: [PATCH 02/18] Fix correctness bugs in model codegen plugin - Fix typos in EnumRenderer FORBIDDEN_WORDS: co_yeild->co_yield, moduel->module - Make generateResultSource/Header protocol-aware (was hardcoded to JSON) - Fix endpoint context param accessor casing (need PascalCase for C++ accessors) --- .../generators/model/EnumRenderer.java | 4 +- .../generators/model/ModelGenerator.java | 71 ++++++++++++------- .../generators/model/EnumRendererTest.java | 8 +++ 3 files changed, 56 insertions(+), 27 deletions(-) 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 577b35f1138..67f414da4f5 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 @@ -215,11 +215,11 @@ private static List getEnumWireValues(Shape enumShape) { "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", "compl", "concept", "const", - "constexpr", "const_cast", "continue", "co_await", "co_return", "co_yeild", + "constexpr", "const_cast", "continue", "co_await", "co_return", "co_yield", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "import", "inline", "int", "long", - "moduel", "mutable", "namespace", "new", "noexcept", "not", "not_eq", + "module", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", "reflexpr", "register", "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", 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 9a74fc74689..2db7355c9d5 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 @@ -325,12 +325,8 @@ private void generateResultHeader(StructureShape shape, OperationShape operation writer.write("template "); writer.write("class AmazonWebServiceResult;"); writer.write(""); - // Forward-declare JsonValue only (not JsonView) - writer.writeNamespaceOpen("Utils"); - writer.writeNamespaceOpen("Json"); - writer.write("class JsonValue;"); - writer.writeNamespaceClose("Json"); - writer.writeNamespaceClose("Utils"); + // Forward-declare serde types based on protocol + renderForwardDeclarations(writer); // Service namespace writer.writeNamespaceOpen(namespace); @@ -347,8 +343,13 @@ private void generateResultHeader(StructureShape shape, OperationShape operation // Export macro + constructors writer.write("$L $L() = default;", exportMacro, className); - writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); - writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + if (protocol.isJsonLike()) { + writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + } else { + writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + } writer.write(""); // Member accessors (NO HasBeenSet for results) @@ -402,32 +403,51 @@ private void generateResultSource(StructureShape shape, OperationShape operation writerDelegator.useFileWriter(fileName, writer -> { writeCopyright(writer); - // Result-specific includes (matching C2J) + // Result-specific includes (protocol-aware) writer.write("#include "); writer.write("#include "); writer.write("#include "); - writer.write("#include "); + if (protocol.isJsonLike()) { + writer.write("#include "); + } else { + writer.write("#include "); + } writer.write("#include "); writer.write("#include ", smithyServiceName, className); writer.write(""); writer.write("#include "); writer.write(""); - writer.write("using namespace Aws::$L::Model;", namespace); - writer.write("using namespace Aws::Utils::Json;"); - writer.write("using namespace Aws::Utils;"); - writer.write("using namespace Aws;"); + if (protocol.isJsonLike()) { + writer.write("using namespace Aws::$L::Model;", namespace); + writer.write("using namespace Aws::Utils::Json;"); + writer.write("using namespace Aws::Utils;"); + writer.write("using namespace Aws;"); + } else { + writer.write("using namespace Aws::$L::Model;", namespace); + writer.write("using namespace Aws::Utils::Xml;"); + writer.write("using namespace Aws::Utils;"); + writer.write("using namespace Aws;"); + } writer.write(""); // AmazonWebServiceResult constructor stub - writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { - writer.write("*this = result;"); - }); - writer.write(""); - - // operator= stub - writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { - writer.write("return *this;"); - }); + if (protocol.isJsonLike()) { + writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { + writer.write("*this = result;"); + }); + writer.write(""); + writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { + writer.write("return *this;"); + }); + } else { + writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { + writer.write("*this = result;"); + }); + writer.write(""); + writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { + writer.write("return *this;"); + }); + } writer.write(""); }); } @@ -604,9 +624,10 @@ public Void booleanNode(software.amazon.smithy.model.node.BooleanNode node) { ContextParamTrait contextParam = member.expectTrait(ContextParamTrait.class); String paramName = contextParam.getName(); String memberName = entry.getKey(); - writer.openBlock("if ($LHasBeenSet()) {", "}", memberName, () -> { + String capitalizedMember = memberName.substring(0, 1).toUpperCase() + memberName.substring(1); + writer.openBlock("if ($LHasBeenSet()) {", "}", capitalizedMember, () -> { writer.write("parameters.emplace_back(Aws::String(\"$L\"), this->Get$L(), Aws::Endpoint::EndpointParameter::ParameterOrigin::OPERATION_CONTEXT);", - paramName, memberName); + paramName, capitalizedMember); }); } } 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 23af527cfb2..23bf9fff966 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 @@ -282,4 +282,12 @@ void sanitizeEnumValue_handlesForbiddenWords() { assertEquals("NULL_", EnumRenderer.sanitizeEnumValue("NULL")); assertEquals("OVERFLOW_", EnumRenderer.sanitizeEnumValue("OVERFLOW")); } + + @Test + void sanitizeEnumValue_handlesCorrectedForbiddenWords() { + // Verify co_yield (was misspelled as co_yeild) is now correctly forbidden + assertEquals("co_yield_", EnumRenderer.sanitizeEnumValue("co_yield")); + // Verify module (was misspelled as moduel) is now correctly forbidden + assertEquals("module_", EnumRenderer.sanitizeEnumValue("module")); + } } From f3f7b145fc438057c7fbadc29426a0a3470d958b Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 21 Jul 2026 13:09:36 -0400 Subject: [PATCH 03/18] Add ModelTransform interface and TransformPipeline Composable model-to-model transform chain that runs before code generation. Service-level transforms will implement ModelTransform and register in the pipeline. --- .../generators/model/ModelTransform.java | 29 +++++++ .../generators/model/TransformPipeline.java | 40 ++++++++++ .../model/TransformPipelineTest.java | 75 +++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java 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 new file mode 100644 index 00000000000..396fb3be6bf --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelTransform.java @@ -0,0 +1,29 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model; + +import software.amazon.smithy.model.Model; +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. + */ +@FunctionalInterface +public interface ModelTransform { + + /** + * Applies this transform to the model. + * + * @param model the current model (may have been modified by earlier transforms) + * @param service the service shape being generated + * @return the transformed model (may be the same instance if no changes needed) + */ + Model apply(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/TransformPipeline.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java new file mode 100644 index 00000000000..4259807bbd1 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipeline.java @@ -0,0 +1,40 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model; + +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; + +import java.util.List; + +/** + * An ordered sequence of {@link ModelTransform}s applied before code generation. + * + *

    Transforms execute in list order. The output of each becomes the input to the next. + * An empty pipeline returns the input model unchanged. + */ +public final class TransformPipeline { + + private final List transforms; + + public TransformPipeline(List transforms) { + this.transforms = List.copyOf(transforms); + } + + /** + * Applies all transforms in order and returns the final model. + * + * @param model the original model + * @param service the service shape being generated + * @return the model after all transforms have been applied + */ + public Model apply(Model model, ServiceShape service) { + Model current = model; + for (ModelTransform transform : transforms) { + current = transform.apply(current, service); + } + return current; + } +} 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 new file mode 100644 index 00000000000..c178d0abb85 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/TransformPipelineTest.java @@ -0,0 +1,75 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class TransformPipelineTest { + + @Test + void emptyPipelineReturnsModelUnchanged() { + 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); + + TransformPipeline pipeline = new TransformPipeline(List.of()); + Model result = pipeline.apply(model, service); + + assertSame(model, result); + } + + @Test + void transformsExecuteInOrder() { + 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<>(); + ModelTransform first = (m, s) -> { executionOrder.add("first"); return m; }; + ModelTransform second = (m, s) -> { executionOrder.add("second"); return m; }; + + TransformPipeline pipeline = new TransformPipeline(List.of(first, second)); + pipeline.apply(model, service); + + assertEquals(List.of("first", "second"), executionOrder); + } + + @Test + void transformReceivesOutputOfPrevious() { + Model original = Model.builder() + .addShape(ServiceShape.builder().id("com.example#TestService").version("2024-01-01").build()) + .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) -> { + // 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); + + // Final result is the model from the last transform + assertSame(withExtra, result); + } +} From 083710b235c0c23693664ec6be2990ac734b0e5d Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 21 Jul 2026 13:53:21 -0400 Subject: [PATCH 04/18] Extract ShapeRenderer interface and per-classification renderers ModelGenerator is now a thin dispatcher that routes classified shapes to registered ShapeRenderer implementations. Adding new classification types (e.g., event streams) requires only a new renderer class, not editing the orchestrator. - EnumShapeRenderer, SubObjectRenderer, RequestRenderer, ResultRenderer - Consolidate isPrimitive() into CppTypeMapper - Promote writeDocComment/collapseWhitespace to public in MemberRenderer - No behavioral change to generated output --- .../generators/model/CppTypeMapper.java | 11 + .../generators/model/MemberRenderer.java | 11 +- .../generators/model/ModelGenerator.java | 613 +----------------- .../generators/model/ShapeRenderer.java | 27 + .../model/renderers/EnumShapeRenderer.java | 45 ++ .../model/renderers/RequestRenderer.java | 264 ++++++++ .../model/renderers/ResultRenderer.java | 231 +++++++ .../model/renderers/SubObjectRenderer.java | 192 ++++++ 8 files changed, 802 insertions(+), 592 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeRenderer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EnumShapeRenderer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java 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 4b7b2fa3059..a8f29d1ca74 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 @@ -108,6 +108,17 @@ public static Optional getDefaultValue(Shape shape) { return Optional.empty(); } + /** + * Returns true if the shape maps to a C++ primitive type (int, long long, bool, double). + * + * @param shape the shape to check + * @return true if the shape is a primitive type + */ + public static boolean isPrimitive(Shape shape) { + return shape.isIntegerShape() || shape.isLongShape() + || shape.isBooleanShape() || shape.isDoubleShape() || shape.isFloatShape(); + } + /** * Returns true if the shape type requires a "has been set" tracking flag. * 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 d2b64a0d0ab..cc9074e1423 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 @@ -4,6 +4,8 @@ */ package com.amazonaws.util.awsclientsmithygenerator.generators.model; +import static com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper.isPrimitive; + import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ListShape; @@ -310,11 +312,6 @@ public static void renderPrivateHasBeenSetFlags(CppWriter writer, StructureShape } } - private static boolean isPrimitive(Shape shape) { - return shape.isIntegerShape() || shape.isLongShape() - || shape.isBooleanShape() || shape.isDoubleShape() || shape.isFloatShape(); - } - private static String decapitalize(String name) { if (name.isEmpty()) return name; return Character.toLowerCase(name.charAt(0)) + name.substring(1); @@ -324,7 +321,7 @@ private static String decapitalize(String name) { "", "", "", "" }; - static void writeDocComment(CppWriter writer, String doc) { + public static void writeDocComment(CppWriter writer, String doc) { writer.write("/**"); String formatted = formatDocumentation(doc); for (String line : formatted.split("\n")) { @@ -333,7 +330,7 @@ static void writeDocComment(CppWriter writer, String doc) { writer.write(" */"); } - static String collapseWhitespace(String text) { + public static String collapseWhitespace(String text) { if (text == null) { return null; } 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 2db7355c9d5..15ba3057476 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 @@ -4,33 +4,25 @@ */ package com.amazonaws.util.awsclientsmithygenerator.generators.model; -import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; 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.ShapeClassifier.RequestInfo; -import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.ResultInfo; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EnumShapeRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.ResultRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.SubObjectRenderer; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.OperationShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.traits.DocumentationTrait; -import software.amazon.smithy.aws.traits.clientendpointdiscovery.ClientEndpointDiscoveryTrait; -import software.amazon.smithy.rulesengine.traits.ContextParamTrait; -import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait; +import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; /** - * Orchestrates C++ model code generation for all classified shapes in a service. + * Orchestrates C++ model code generation by dispatching classified shapes + * to registered {@link ShapeRenderer} instances. * - *

    Dispatches to {@link EnumRenderer}, {@link MemberRenderer}, and {@link SerdeStub} - * to produce header and source files for each model shape. + *

    To add generation for a new shape classification (e.g., event streams), + * implement {@link ShapeRenderer} and register it in {@link #buildRenderers}. */ public class ModelGenerator { private final Model model; @@ -57,583 +49,34 @@ public ModelGenerator(Model model, ServiceShape service, CppWriterDelegator writ public void generateAll() { ClassifiedShapes classified = ShapeClassifier.classify(model, service); + List renderers = buildRenderers(classified); - // Generate enum headers + sources - for (Shape enumShape : classified.enums()) { - generateEnumHeader(enumShape); - generateEnumSource(enumShape); + for (ShapeRenderer renderer : renderers) { + renderer.render(writerDelegator); } - - // Generate sub-object headers + sources - for (Shape shape : classified.subObjects()) { - if (shape.isStructureShape()) { - generateSubObjectHeader(shape.asStructureShape().get()); - generateSubObjectSource(shape.asStructureShape().get()); - } - } - - // Generate request headers + sources (Input -> Request) - for (RequestInfo reqInfo : classified.requests()) { - generateRequestHeader(reqInfo.shape(), reqInfo.operation()); - generateRequestSource(reqInfo.shape(), reqInfo.operation()); - } - - // Generate result headers + sources (Output -> Result) - for (ResultInfo resInfo : classified.results()) { - generateResultHeader(resInfo.shape(), resInfo.operation()); - generateResultSource(resInfo.shape(), resInfo.operation()); - } - } - - private void generateEnumHeader(Shape enumShape) { - String fileName = "include/aws/" + smithyServiceName + "/model/" + enumShape.getId().getName() + ".h"; - writerDelegator.useFileWriter(fileName, writer -> - EnumRenderer.renderHeader(writer, enumShape, namespace, exportMacro, smithyServiceName)); - } - - private void generateEnumSource(Shape enumShape) { - String fileName = "source/model/" + enumShape.getId().getName() + ".cpp"; - writerDelegator.useFileWriter(fileName, writer -> - EnumRenderer.renderSource(writer, enumShape, namespace, exportMacro, smithyServiceName)); - } - - private void generateSubObjectHeader(StructureShape shape) { - String className = shape.getId().getName(); - String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; - writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); - writer.write("#pragma once"); - - // Includes - renderIncludes(writer, shape); - - // Include unless ALL members are primitives - boolean allPrimitive = shape.getAllMembers().values().stream() - .map(m -> model.expectShape(m.getTarget())) - .allMatch(ModelGenerator::isPrimitive); - if (!allPrimitive) { - writer.write(""); - writer.write("#include "); - } - writer.write(""); - - // Open namespace Aws - writer.writeNamespaceOpen("Aws"); - - // Forward declarations for serde types - renderForwardDeclarations(writer); - - // Service namespace - writer.writeNamespaceOpen(namespace); - writer.writeNamespaceOpen("Model"); - writer.write(""); - - // Class-level documentation comment - renderClassDocComment(writer, shape); - - // Class declaration - writer.openBlock("class $L {", "};", className, () -> { - writer.write("public:"); - - // Serde declarations - SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className); - writer.write(""); - - // Member accessors (with HasBeenSet) - MemberRenderer.renderPublicSection(writer, shape, model, exportMacro, className); - - // Private section - writer.dedent(); - writer.write("private:"); - writer.indent(); - MemberRenderer.renderPrivateSection(writer, shape, model); - }); - writer.write(""); - - // Namespace close - writer.writeNamespaceClose("Model"); - writer.writeNamespaceClose(namespace); - writer.writeNamespaceClose("Aws"); - }); - } - - private void generateSubObjectSource(StructureShape shape) { - String className = shape.getId().getName(); - String fileName = "source/model/" + className + ".cpp"; - writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); - - // Includes - writeSourceIncludes(writer, className, false, false); - - // Explicit namespace blocks for sub-objects - writer.writeNamespaceOpen("Aws"); - writer.writeNamespaceOpen(namespace); - writer.writeNamespaceOpen("Model"); - writer.write(""); - - // Serde implementation - SerdeStub.renderSourceImplementation(writer, shape, model, protocol, - className, serviceName, smithyServiceName); - writer.write(""); - - // Namespace close - writer.writeNamespaceClose("Model"); - writer.writeNamespaceClose(namespace); - writer.writeNamespaceClose("Aws"); - }); - } - - private void generateRequestHeader(StructureShape shape, OperationShape operation) { - String className = getRequestClassName(operation); - String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; - writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); - writer.write("#pragma once"); - - // Includes - request needs the service request base class - Set includes = new TreeSet<>(); - includes.add(""); - includes.add(""); - List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); - includes.addAll(memberIncludes); - for (String include : includes) { - writer.write("#include $L", include); - } - - // Include unless ALL members are primitives - boolean allPrimitive = shape.getAllMembers().values().stream() - .map(m -> model.expectShape(m.getTarget())) - .allMatch(ModelGenerator::isPrimitive); - if (!allPrimitive) { - writer.write(""); - writer.write("#include "); - } - writer.write(""); - - // Open namespace Aws - no forward declarations for requests (base class provides them) - writer.writeNamespaceOpen("Aws"); - writer.writeNamespaceOpen(namespace); - writer.writeNamespaceOpen("Model"); - writer.write(""); - - // Class-level doc comment - renderClassDocComment(writer, shape); - - // Class declaration - inherits from service request - String baseClass = namespace + "Request"; - writer.openBlock("class $L : public $L {", "};", className, baseClass, () -> { - writer.write("public:"); - - // Export macro + default constructor - writer.write("$L $L() = default;", exportMacro, className); - writer.write(""); - - // Declare service-specific overrides - writer.write("// Service request name is the Operation name which will send this request out,"); - writer.write("// each operation should has unique request name, so that we can get operation's name from this request."); - writer.write("// Note: this is not true for response, multiple operations may have the same response name,"); - writer.write("// so we can not get operation's name from response."); - writer.write("inline virtual const char* GetServiceRequestName() const override { return \"$L\"; }", operation.getId().getName()); - writer.write(""); - writer.write("$L Aws::String SerializePayload() const override;", exportMacro); - writer.write(""); - writer.write("$L Aws::Http::HeaderValueCollection GetRequestSpecificHeaders() const override;", exportMacro); - - // Endpoint context params if needed - if (hasEndpointContextParams(operation, shape)) { - writer.write(""); - writer.write("/**"); - writer.write(" * Helper function to collect parameters (configurable and static hardcoded) required for endpoint computation."); - writer.write(" */"); - writer.write("$L EndpointParameters GetEndpointContextParams() const override;", exportMacro); - } - - writer.write(""); - - // Member accessors (with HasBeenSet) - MemberRenderer.renderPublicSection(writer, shape, model, exportMacro, className); - - // Private section - only emit if there are members - if (!shape.getAllMembers().isEmpty()) { - writer.dedent(); - writer.write("private:"); - writer.indent(); - MemberRenderer.renderPrivateSection(writer, shape, model); - } - }); - writer.write(""); - - // Namespace close - writer.writeNamespaceClose("Model"); - writer.writeNamespaceClose(namespace); - writer.writeNamespaceClose("Aws"); - }); - } - - private void generateRequestSource(StructureShape shape, OperationShape operation) { - String className = getRequestClassName(operation); - String fileName = "source/model/" + className + ".cpp"; - writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); - - // Includes and using namespace (not explicit namespace blocks) - writeSourceIncludes(writer, className, true, false); - - // SerializePayload stub - writer.write("Aws::String $L::SerializePayload() const { return \"{}\"; }", className); - writer.write(""); - - // GetRequestSpecificHeaders stub - writer.write("Aws::Http::HeaderValueCollection $L::GetRequestSpecificHeaders() const { return {}; }", className); - - // GetEndpointContextParams if needed - if (hasEndpointContextParams(operation, shape)) { - writer.write(""); - renderEndpointContextParams(writer, className, operation, shape); - } - writer.write(""); - }); - } - - private void generateResultHeader(StructureShape shape, OperationShape operation) { - String className = getResultClassName(operation); - String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; - writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); - writer.write("#pragma once"); - - // Includes — match C2J: HttpResponse, AWSString, service exports, member types - Set includes = new TreeSet<>(); - includes.add(""); - includes.add(""); - includes.add(""); - List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); - includes.addAll(memberIncludes); - for (String include : includes) { - writer.write("#include $L", include); - } - - // Always include for results (synthetic RequestId is Aws::String) - writer.write(""); - writer.write("#include "); - writer.write(""); - - // Open namespace Aws with forward declarations matching C2J pattern - writer.writeNamespaceOpen("Aws"); - // Forward-declare AmazonWebServiceResult template - writer.write("template "); - writer.write("class AmazonWebServiceResult;"); - writer.write(""); - // Forward-declare serde types based on protocol - renderForwardDeclarations(writer); - - // Service namespace - writer.writeNamespaceOpen(namespace); - writer.writeNamespaceOpen("Model"); - - // Class-level doc comment — only emit if shape has documentation (C2J doesn't emit empty block for results) - if (shape.getTrait(DocumentationTrait.class).isPresent()) { - renderClassDocComment(writer, shape); - } - - // Class declaration - writer.openBlock("class $L {", "};", className, () -> { - writer.write("public:"); - - // Export macro + constructors - writer.write("$L $L() = default;", exportMacro, className); - if (protocol.isJsonLike()) { - writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); - writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); - } else { - writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); - writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); - } - writer.write(""); - - // Member accessors (NO HasBeenSet for results) - MemberRenderer.renderPublicSectionForResult(writer, shape, model, exportMacro, className); - - // Synthetic members: RequestId - writer.write(""); - writer.write("///@{"); - writer.write(""); - writer.write("inline const Aws::String& GetRequestId() const { return m_requestId; }"); - writer.write("template "); - writer.openBlock("void SetRequestId(RequestIdT&& value) {", "}", () -> { - writer.write("m_requestIdHasBeenSet = true;"); - writer.write("m_requestId = std::forward(value);"); - }); - writer.write("template "); - writer.openBlock("$L& WithRequestId(RequestIdT&& value) {", "}", className, () -> { - writer.write("SetRequestId(std::forward(value));"); - writer.write("return *this;"); - }); - writer.write("///@}"); - - // HttpResponseCode accessor - writer.write("inline Aws::Http::HttpResponseCode GetHttpResponseCode() const { return m_HttpResponseCode; }"); - writer.write(""); - - // Private section - writer.dedent(); - writer.write("private:"); - writer.indent(); - MemberRenderer.renderPrivateDataMembers(writer, shape, model); - writer.write(""); - writer.write("Aws::String m_requestId;"); - writer.write("Aws::Http::HttpResponseCode m_HttpResponseCode;"); - // HasBeenSet flags for all members + requestId - MemberRenderer.renderPrivateHasBeenSetFlags(writer, shape, model); - writer.write("bool m_requestIdHasBeenSet = false;"); - }); - writer.write(""); - - // Namespace close - writer.writeNamespaceClose("Model"); - writer.writeNamespaceClose(namespace); - writer.writeNamespaceClose("Aws"); - }); - } - - private void generateResultSource(StructureShape shape, OperationShape operation) { - String className = getResultClassName(operation); - String fileName = "source/model/" + className + ".cpp"; - writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); - - // Result-specific includes (protocol-aware) - writer.write("#include "); - writer.write("#include "); - writer.write("#include "); - if (protocol.isJsonLike()) { - writer.write("#include "); - } else { - writer.write("#include "); - } - writer.write("#include "); - writer.write("#include ", smithyServiceName, className); - writer.write(""); - writer.write("#include "); - writer.write(""); - if (protocol.isJsonLike()) { - writer.write("using namespace Aws::$L::Model;", namespace); - writer.write("using namespace Aws::Utils::Json;"); - writer.write("using namespace Aws::Utils;"); - writer.write("using namespace Aws;"); - } else { - writer.write("using namespace Aws::$L::Model;", namespace); - writer.write("using namespace Aws::Utils::Xml;"); - writer.write("using namespace Aws::Utils;"); - writer.write("using namespace Aws;"); - } - writer.write(""); - - // AmazonWebServiceResult constructor stub - if (protocol.isJsonLike()) { - writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { - writer.write("*this = result;"); - }); - writer.write(""); - writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { - writer.write("return *this;"); - }); - } else { - writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { - writer.write("*this = result;"); - }); - writer.write(""); - writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", className, className, () -> { - writer.write("return *this;"); - }); - } - writer.write(""); - }); - } - - private String getRequestClassName(OperationShape operation) { - return operation.getId().getName() + "Request"; } - private String getResultClassName(OperationShape operation) { - return operation.getId().getName() + "Result"; - } + private List buildRenderers(ClassifiedShapes classified) { + List renderers = new ArrayList<>(); - private void writeCopyright(CppWriter writer) { - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); - } + renderers.add(new EnumShapeRenderer( + classified.enums(), namespace, exportMacro, smithyServiceName)); - /** - * Writes source file includes and using declarations. - * - * @param writer the CppWriter - * @param className the class name (for the model include) - * @param useModelNamespace if true, emit "using namespace Aws::{Namespace}::Model;" instead of explicit namespace blocks - * @param useAwsNamespace if true, also emit "using namespace Aws;" (for result sources) - */ - private void writeSourceIncludes(CppWriter writer, String className, - boolean useModelNamespace, boolean useAwsNamespace) { - // Includes - if (protocol.isJsonLike()) { - writer.write("#include "); - } else { - writer.write("#include "); - } - writer.write("#include ", smithyServiceName, className); - writer.write(""); - writer.write("#include "); - writer.write(""); - - // Using declarations - if (useModelNamespace) { - writer.write("using namespace Aws::$L::Model;", namespace); - } - if (protocol.isJsonLike()) { - writer.write("using namespace Aws::Utils::Json;"); - writer.write("using namespace Aws::Utils;"); - } else { - writer.write("using namespace Aws::Utils::Xml;"); - writer.write("using namespace Aws::Utils;"); - } - if (useAwsNamespace) { - writer.write("using namespace Aws;"); - } - writer.write(""); - } - - private void renderIncludes(CppWriter writer, StructureShape shape) { - Set includes = new TreeSet<>(); // sorted for deterministic output - includes.add(""); - - List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); - includes.addAll(memberIncludes); - - for (String include : includes) { - writer.write("#include $L", include); - } - } - - private void renderForwardDeclarations(CppWriter writer) { - if (protocol.isJsonLike()) { - writer.writeNamespaceOpen("Utils"); - writer.writeNamespaceOpen("Json"); - writer.write("class JsonValue;"); - writer.write("class JsonView;"); - writer.writeNamespaceClose("Json"); - writer.writeNamespaceClose("Utils"); - } else { - writer.writeNamespaceOpen("Utils"); - writer.writeNamespaceOpen("Xml"); - writer.write("class XmlNode;"); - writer.writeNamespaceClose("Xml"); - writer.writeNamespaceClose("Utils"); - } - } - - private static boolean isPrimitive(Shape shape) { - return shape.isIntegerShape() || shape.isLongShape() - || shape.isBooleanShape() || shape.isDoubleShape() - || shape.isFloatShape(); - } - - /** - * Renders the class-level documentation comment. - * If the shape has no documentation, emits an empty doc block to match C2J output. - */ - private void renderClassDocComment(CppWriter writer, StructureShape shape) { - if (shape.getTrait(DocumentationTrait.class).isPresent()) { - String docText = MemberRenderer.collapseWhitespace( - shape.getTrait(DocumentationTrait.class).get().getValue()); - String version = service.getVersion(); - String seeAlso = String.format( - "

    See Also:

    AWS API Reference

    ", - smithyServiceName, version, shape.getId().getName()); - MemberRenderer.writeDocComment(writer, docText + seeAlso); - } else { - // Empty doc block matching C2J output for undocumented shapes - writer.write("/**"); - writer.write(" */"); - } - } - - /** - * Checks if an operation has endpoint context parameters (static or member-level). - */ - private boolean hasEndpointContextParams(OperationShape operation, StructureShape requestShape) { - // Check for @staticContextParams on the operation - if (operation.hasTrait(StaticContextParamsTrait.class)) { - return true; - } - // Check for @contextParam on any member of the request shape - for (MemberShape member : requestShape.getAllMembers().values()) { - if (member.hasTrait(ContextParamTrait.class)) { - return true; - } - } - return false; - } + renderers.add(new SubObjectRenderer( + classified.subObjects(), model, service, protocol, + namespace, exportMacro, serviceName, smithyServiceName)); - /** - * Renders the GetEndpointContextParams function body. - */ - private void renderEndpointContextParams(CppWriter writer, String className, - OperationShape operation, StructureShape shape) { - writer.openBlock("$L::EndpointParameters $L::GetEndpointContextParams() const {", "}", - className, className, () -> { - writer.write("EndpointParameters parameters;"); + renderers.add(new RequestRenderer( + classified.requests(), model, service, protocol, + namespace, exportMacro, smithyServiceName)); - // Static context params - if (operation.hasTrait(StaticContextParamsTrait.class)) { - writer.write("// Static context parameters"); - StaticContextParamsTrait staticParams = operation.expectTrait(StaticContextParamsTrait.class); - staticParams.getParameters().forEach((name, def) -> { - def.getValue().accept(new software.amazon.smithy.model.node.NodeVisitor.Default() { - @Override - protected Void getDefault(software.amazon.smithy.model.node.Node node) { - return null; - } - @Override - public Void stringNode(software.amazon.smithy.model.node.StringNode node) { - writer.write("parameters.emplace_back(Aws::String(\"$L\"), \"$L\", Aws::Endpoint::EndpointParameter::ParameterOrigin::STATIC_CONTEXT);", - name, node.getValue()); - return null; - } - @Override - public Void booleanNode(software.amazon.smithy.model.node.BooleanNode node) { - writer.write("parameters.emplace_back(Aws::String(\"$L\"), $L, Aws::Endpoint::EndpointParameter::ParameterOrigin::STATIC_CONTEXT);", - name, node.getValue()); - return null; - } - }); - }); - } + renderers.add(new ResultRenderer( + classified.results(), model, service, protocol, + namespace, exportMacro, smithyServiceName)); - // Operation context params (from members with @contextParam) - boolean hasContextParams = shape.getAllMembers().values().stream() - .anyMatch(m -> m.hasTrait(ContextParamTrait.class)); - if (hasContextParams) { - writer.write("// Operation context parameters"); - for (Map.Entry entry : shape.getAllMembers().entrySet()) { - MemberShape member = entry.getValue(); - if (member.hasTrait(ContextParamTrait.class)) { - ContextParamTrait contextParam = member.expectTrait(ContextParamTrait.class); - String paramName = contextParam.getName(); - String memberName = entry.getKey(); - String capitalizedMember = memberName.substring(0, 1).toUpperCase() + memberName.substring(1); - writer.openBlock("if ($LHasBeenSet()) {", "}", capitalizedMember, () -> { - writer.write("parameters.emplace_back(Aws::String(\"$L\"), this->Get$L(), Aws::Endpoint::EndpointParameter::ParameterOrigin::OPERATION_CONTEXT);", - paramName, capitalizedMember); - }); - } - } - } + // Future: event stream renderers will be registered here + // renderers.add(new EventStreamRenderer(...)); - writer.write("return parameters;"); - }); + return renderers; } } 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 new file mode 100644 index 00000000000..47ae2d878c9 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeRenderer.java @@ -0,0 +1,27 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model; + +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}. + */ +public interface ShapeRenderer { + + /** + * Renders all C++ files (headers and sources) for the shapes this renderer handles. + * + * @param writerDelegator the file writer to emit generated code through + */ + void render(CppWriterDelegator writerDelegator); +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EnumShapeRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EnumShapeRenderer.java new file mode 100644 index 00000000000..5e187dcce75 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EnumShapeRenderer.java @@ -0,0 +1,45 @@ +/** + * 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.EnumRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import software.amazon.smithy.model.shapes.Shape; + +import java.util.List; + +/** + * Renders C++ enum class headers and mapper source files for all classified enum shapes. + */ +public final class EnumShapeRenderer implements ShapeRenderer { + + private final List enumShapes; + private final String namespace; + private final String exportMacro; + private final String smithyServiceName; + + public EnumShapeRenderer(List enumShapes, String namespace, + String exportMacro, String smithyServiceName) { + this.enumShapes = enumShapes; + this.namespace = namespace; + this.exportMacro = exportMacro; + this.smithyServiceName = smithyServiceName; + } + + @Override + public void render(CppWriterDelegator writerDelegator) { + for (Shape enumShape : enumShapes) { + String name = enumShape.getId().getName(); + String headerFile = "include/aws/" + smithyServiceName + "/model/" + name + ".h"; + writerDelegator.useFileWriter(headerFile, writer -> + EnumRenderer.renderHeader(writer, enumShape, namespace, exportMacro, smithyServiceName)); + + String sourceFile = "source/model/" + name + ".cpp"; + writerDelegator.useFileWriter(sourceFile, writer -> + EnumRenderer.renderSource(writer, enumShape, namespace, exportMacro, smithyServiceName)); + } + } +} 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 new file mode 100644 index 00000000000..888880b4da7 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java @@ -0,0 +1,264 @@ +/** + * 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.CppTypeMapper; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.RequestInfo; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.rulesengine.traits.ContextParamTrait; +import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * Renders C++ headers and sources for request (operation input) shapes. + */ +public final class RequestRenderer implements ShapeRenderer { + + private final List requests; + private final Model model; + private final ServiceShape service; + private final Protocol protocol; + private final String namespace; + private final String exportMacro; + private final String smithyServiceName; + + public RequestRenderer(List requests, Model model, ServiceShape service, + Protocol protocol, String namespace, String exportMacro, + String smithyServiceName) { + this.requests = requests; + this.model = model; + this.service = service; + this.protocol = protocol; + this.namespace = namespace; + this.exportMacro = exportMacro; + this.smithyServiceName = smithyServiceName; + } + + @Override + public void render(CppWriterDelegator writerDelegator) { + for (RequestInfo reqInfo : requests) { + renderHeader(writerDelegator, reqInfo.shape(), reqInfo.operation()); + renderSource(writerDelegator, reqInfo.shape(), reqInfo.operation()); + } + } + + private void renderHeader(CppWriterDelegator writerDelegator, + StructureShape shape, OperationShape operation) { + String className = operation.getId().getName() + "Request"; + String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + + Set includes = new TreeSet<>(); + includes.add(""); + includes.add(""); + List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); + includes.addAll(memberIncludes); + for (String include : includes) { + writer.write("#include $L", include); + } + + boolean allPrimitive = shape.getAllMembers().values().stream() + .map(m -> model.expectShape(m.getTarget())) + .allMatch(CppTypeMapper::isPrimitive); + if (!allPrimitive) { + writer.write(""); + writer.write("#include "); + } + writer.write(""); + + writer.writeNamespaceOpen("Aws"); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + writer.write(""); + + renderClassDocComment(writer, shape); + + String baseClass = namespace + "Request"; + writer.openBlock("class $L : public $L {", "};", className, baseClass, () -> { + writer.write("public:"); + writer.write("$L $L() = default;", exportMacro, className); + writer.write(""); + + writer.write("// Service request name is the Operation name which will send this request out,"); + writer.write("// each operation should has unique request name, so that we can get operation's name from this request."); + writer.write("// Note: this is not true for response, multiple operations may have the same response name,"); + writer.write("// so we can not get operation's name from response."); + writer.write("inline virtual const char* GetServiceRequestName() const override { return \"$L\"; }", + operation.getId().getName()); + writer.write(""); + writer.write("$L Aws::String SerializePayload() const override;", exportMacro); + writer.write(""); + writer.write("$L Aws::Http::HeaderValueCollection GetRequestSpecificHeaders() const override;", exportMacro); + + if (hasEndpointContextParams(operation, shape)) { + writer.write(""); + writer.write("/**"); + writer.write(" * Helper function to collect parameters (configurable and static hardcoded) required for endpoint computation."); + writer.write(" */"); + writer.write("$L EndpointParameters GetEndpointContextParams() const override;", exportMacro); + } + + writer.write(""); + MemberRenderer.renderPublicSection(writer, shape, model, exportMacro, className); + + if (!shape.getAllMembers().isEmpty()) { + writer.dedent(); + writer.write("private:"); + writer.indent(); + MemberRenderer.renderPrivateSection(writer, shape, model); + } + }); + writer.write(""); + + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + private void renderSource(CppWriterDelegator writerDelegator, + StructureShape shape, OperationShape operation) { + String className = operation.getId().getName() + "Request"; + String fileName = "source/model/" + className + ".cpp"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + + if (protocol.isJsonLike()) { + writer.write("#include "); + } else { + writer.write("#include "); + } + writer.write("#include ", smithyServiceName, className); + writer.write(""); + writer.write("#include "); + writer.write(""); + + writer.write("using namespace Aws::$L::Model;", namespace); + if (protocol.isJsonLike()) { + writer.write("using namespace Aws::Utils::Json;"); + writer.write("using namespace Aws::Utils;"); + } else { + writer.write("using namespace Aws::Utils::Xml;"); + writer.write("using namespace Aws::Utils;"); + } + writer.write(""); + + writer.write("Aws::String $L::SerializePayload() const { return \"{}\"; }", className); + writer.write(""); + writer.write("Aws::Http::HeaderValueCollection $L::GetRequestSpecificHeaders() const { return {}; }", className); + + if (hasEndpointContextParams(operation, shape)) { + writer.write(""); + renderEndpointContextParams(writer, className, operation, shape); + } + writer.write(""); + }); + } + + private boolean hasEndpointContextParams(OperationShape operation, StructureShape shape) { + if (operation.hasTrait(StaticContextParamsTrait.class)) { + return true; + } + for (MemberShape member : shape.getAllMembers().values()) { + if (member.hasTrait(ContextParamTrait.class)) { + return true; + } + } + return false; + } + + private void renderEndpointContextParams(CppWriter writer, String className, + OperationShape operation, StructureShape shape) { + writer.openBlock("$L::EndpointParameters $L::GetEndpointContextParams() const {", "}", + className, className, () -> { + writer.write("EndpointParameters parameters;"); + + if (operation.hasTrait(StaticContextParamsTrait.class)) { + writer.write("// Static context parameters"); + StaticContextParamsTrait staticParams = operation.expectTrait(StaticContextParamsTrait.class); + staticParams.getParameters().forEach((name, def) -> { + def.getValue().accept(new software.amazon.smithy.model.node.NodeVisitor.Default() { + @Override + protected Void getDefault(software.amazon.smithy.model.node.Node node) { + return null; + } + @Override + public Void stringNode(software.amazon.smithy.model.node.StringNode node) { + writer.write("parameters.emplace_back(Aws::String(\"$L\"), \"$L\", Aws::Endpoint::EndpointParameter::ParameterOrigin::STATIC_CONTEXT);", + name, node.getValue()); + return null; + } + @Override + public Void booleanNode(software.amazon.smithy.model.node.BooleanNode node) { + writer.write("parameters.emplace_back(Aws::String(\"$L\"), $L, Aws::Endpoint::EndpointParameter::ParameterOrigin::STATIC_CONTEXT);", + name, node.getValue()); + return null; + } + }); + }); + } + + boolean hasContextParams = shape.getAllMembers().values().stream() + .anyMatch(m -> m.hasTrait(ContextParamTrait.class)); + if (hasContextParams) { + writer.write("// Operation context parameters"); + for (Map.Entry entry : shape.getAllMembers().entrySet()) { + MemberShape member = entry.getValue(); + if (member.hasTrait(ContextParamTrait.class)) { + ContextParamTrait contextParam = member.expectTrait(ContextParamTrait.class); + String paramName = contextParam.getName(); + String memberName = entry.getKey(); + String capitalizedMember = memberName.substring(0, 1).toUpperCase() + memberName.substring(1); + writer.openBlock("if ($LHasBeenSet()) {", "}", capitalizedMember, () -> { + writer.write("parameters.emplace_back(Aws::String(\"$L\"), this->Get$L(), Aws::Endpoint::EndpointParameter::ParameterOrigin::OPERATION_CONTEXT);", + paramName, capitalizedMember); + }); + } + } + } + + writer.write("return parameters;"); + }); + } + + private void renderClassDocComment(CppWriter writer, StructureShape shape) { + if (shape.getTrait(DocumentationTrait.class).isPresent()) { + String docText = MemberRenderer.collapseWhitespace( + shape.getTrait(DocumentationTrait.class).get().getValue()); + String version = service.getVersion(); + String seeAlso = String.format( + "

    See Also:

    AWS API Reference

    ", + smithyServiceName, version, shape.getId().getName()); + MemberRenderer.writeDocComment(writer, docText + seeAlso); + } else { + writer.write("/**"); + writer.write(" */"); + } + } + + private void writeCopyright(CppWriter writer) { + writer.write("/**"); + writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + writer.write(" * SPDX-License-Identifier: Apache-2.0."); + writer.write(" */"); + writer.write(""); + } +} 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 new file mode 100644 index 00000000000..4ffa1c62d89 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/ResultRenderer.java @@ -0,0 +1,231 @@ +/** + * 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.CppTypeMapper; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.ResultInfo; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +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.StructureShape; +import software.amazon.smithy.model.traits.DocumentationTrait; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Renders C++ headers and sources for result (operation output) shapes. + */ +public final class ResultRenderer implements ShapeRenderer { + + private final List results; + private final Model model; + private final ServiceShape service; + private final Protocol protocol; + private final String namespace; + private final String exportMacro; + private final String smithyServiceName; + + public ResultRenderer(List results, Model model, ServiceShape service, + Protocol protocol, String namespace, String exportMacro, + String smithyServiceName) { + this.results = results; + this.model = model; + this.service = service; + this.protocol = protocol; + this.namespace = namespace; + this.exportMacro = exportMacro; + this.smithyServiceName = smithyServiceName; + } + + @Override + public void render(CppWriterDelegator writerDelegator) { + for (ResultInfo resInfo : results) { + renderHeader(writerDelegator, resInfo.shape(), resInfo.operation()); + renderSource(writerDelegator, resInfo.shape(), resInfo.operation()); + } + } + + private void renderHeader(CppWriterDelegator writerDelegator, + StructureShape shape, OperationShape operation) { + String className = operation.getId().getName() + "Result"; + String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + + Set includes = new TreeSet<>(); + includes.add(""); + includes.add(""); + includes.add(""); + List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); + includes.addAll(memberIncludes); + for (String include : includes) { + writer.write("#include $L", include); + } + + writer.write(""); + writer.write("#include "); + writer.write(""); + + writer.writeNamespaceOpen("Aws"); + writer.write("template "); + writer.write("class AmazonWebServiceResult;"); + writer.write(""); + renderForwardDeclarations(writer); + + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + + if (shape.getTrait(DocumentationTrait.class).isPresent()) { + renderClassDocComment(writer, shape); + } + + writer.openBlock("class $L {", "};", className, () -> { + writer.write("public:"); + writer.write("$L $L() = default;", exportMacro, className); + if (protocol.isJsonLike()) { + writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + } else { + writer.write("$L $L(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + writer.write("$L $L& operator=(const Aws::AmazonWebServiceResult& result);", exportMacro, className); + } + writer.write(""); + + MemberRenderer.renderPublicSectionForResult(writer, shape, model, exportMacro, className); + + writer.write(""); + writer.write("///@{"); + writer.write(""); + writer.write("inline const Aws::String& GetRequestId() const { return m_requestId; }"); + writer.write("template "); + writer.openBlock("void SetRequestId(RequestIdT&& value) {", "}", () -> { + writer.write("m_requestIdHasBeenSet = true;"); + writer.write("m_requestId = std::forward(value);"); + }); + writer.write("template "); + writer.openBlock("$L& WithRequestId(RequestIdT&& value) {", "}", className, () -> { + writer.write("SetRequestId(std::forward(value));"); + writer.write("return *this;"); + }); + writer.write("///@}"); + + writer.write("inline Aws::Http::HttpResponseCode GetHttpResponseCode() const { return m_HttpResponseCode; }"); + writer.write(""); + + writer.dedent(); + writer.write("private:"); + writer.indent(); + MemberRenderer.renderPrivateDataMembers(writer, shape, model); + writer.write(""); + writer.write("Aws::String m_requestId;"); + writer.write("Aws::Http::HttpResponseCode m_HttpResponseCode;"); + MemberRenderer.renderPrivateHasBeenSetFlags(writer, shape, model); + writer.write("bool m_requestIdHasBeenSet = false;"); + }); + writer.write(""); + + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + private void renderSource(CppWriterDelegator writerDelegator, + StructureShape shape, OperationShape operation) { + String className = operation.getId().getName() + "Result"; + String fileName = "source/model/" + className + ".cpp"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + + writer.write("#include "); + writer.write("#include "); + writer.write("#include "); + if (protocol.isJsonLike()) { + writer.write("#include "); + } else { + writer.write("#include "); + } + writer.write("#include "); + writer.write("#include ", smithyServiceName, className); + writer.write(""); + writer.write("#include "); + writer.write(""); + if (protocol.isJsonLike()) { + writer.write("using namespace Aws::$L::Model;", namespace); + writer.write("using namespace Aws::Utils::Json;"); + writer.write("using namespace Aws::Utils;"); + writer.write("using namespace Aws;"); + } else { + writer.write("using namespace Aws::$L::Model;", namespace); + writer.write("using namespace Aws::Utils::Xml;"); + writer.write("using namespace Aws::Utils;"); + writer.write("using namespace Aws;"); + } + writer.write(""); + + if (protocol.isJsonLike()) { + writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", + className, className, () -> writer.write("*this = result;")); + writer.write(""); + writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", + className, className, () -> writer.write("return *this;")); + } else { + writer.openBlock("$L::$L(const Aws::AmazonWebServiceResult& result) {", "}", + className, className, () -> writer.write("*this = result;")); + writer.write(""); + writer.openBlock("$L& $L::operator=(const Aws::AmazonWebServiceResult& result) {", "}", + className, className, () -> writer.write("return *this;")); + } + writer.write(""); + }); + } + + private void renderForwardDeclarations(CppWriter writer) { + if (protocol.isJsonLike()) { + writer.writeNamespaceOpen("Utils"); + writer.writeNamespaceOpen("Json"); + writer.write("class JsonValue;"); + writer.writeNamespaceClose("Json"); + writer.writeNamespaceClose("Utils"); + } else { + writer.writeNamespaceOpen("Utils"); + writer.writeNamespaceOpen("Xml"); + writer.write("class XmlDocument;"); + writer.writeNamespaceClose("Xml"); + writer.writeNamespaceClose("Utils"); + } + } + + private void renderClassDocComment(CppWriter writer, StructureShape shape) { + if (shape.getTrait(DocumentationTrait.class).isPresent()) { + String docText = MemberRenderer.collapseWhitespace( + shape.getTrait(DocumentationTrait.class).get().getValue()); + String version = service.getVersion(); + String seeAlso = String.format( + "

    See Also:

    AWS API Reference

    ", + smithyServiceName, version, shape.getId().getName()); + MemberRenderer.writeDocComment(writer, docText + seeAlso); + } else { + writer.write("/**"); + writer.write(" */"); + } + } + + private void writeCopyright(CppWriter writer) { + writer.write("/**"); + writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + writer.write(" * SPDX-License-Identifier: Apache-2.0."); + writer.write(" */"); + writer.write(""); + } +} 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 new file mode 100644 index 00000000000..5038e949c16 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/SubObjectRenderer.java @@ -0,0 +1,192 @@ +/** + * 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.CppTypeMapper; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.SerdeStub; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +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.StructureShape; +import software.amazon.smithy.model.traits.DocumentationTrait; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Renders C++ headers and sources for sub-object (intermediate structure) shapes. + */ +public final class SubObjectRenderer implements ShapeRenderer { + + private final List subObjects; + private final Model model; + private final ServiceShape service; + private final Protocol protocol; + private final String namespace; + private final String exportMacro; + private final String serviceName; + private final String smithyServiceName; + + public SubObjectRenderer(List subObjects, Model model, ServiceShape service, + Protocol protocol, String namespace, String exportMacro, + String serviceName, String smithyServiceName) { + this.subObjects = subObjects; + this.model = model; + this.service = service; + this.protocol = protocol; + this.namespace = namespace; + this.exportMacro = exportMacro; + this.serviceName = serviceName; + this.smithyServiceName = smithyServiceName; + } + + @Override + public void render(CppWriterDelegator writerDelegator) { + for (Shape shape : subObjects) { + if (shape.isStructureShape()) { + StructureShape struct = shape.asStructureShape().get(); + renderHeader(writerDelegator, struct); + renderSource(writerDelegator, struct); + } + } + } + + private void renderHeader(CppWriterDelegator writerDelegator, StructureShape shape) { + String className = shape.getId().getName(); + String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + + // Includes + Set includes = new TreeSet<>(); + includes.add(""); + List memberIncludes = CppTypeMapper.getIncludesForShape(shape, model, smithyServiceName); + includes.addAll(memberIncludes); + for (String include : includes) { + writer.write("#include $L", include); + } + + boolean allPrimitive = shape.getAllMembers().values().stream() + .map(m -> model.expectShape(m.getTarget())) + .allMatch(CppTypeMapper::isPrimitive); + if (!allPrimitive) { + writer.write(""); + writer.write("#include "); + } + writer.write(""); + + writer.writeNamespaceOpen("Aws"); + renderForwardDeclarations(writer); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + writer.write(""); + + renderClassDocComment(writer, shape); + + writer.openBlock("class $L {", "};", className, () -> { + writer.write("public:"); + SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className); + writer.write(""); + MemberRenderer.renderPublicSection(writer, shape, model, exportMacro, className); + writer.dedent(); + writer.write("private:"); + writer.indent(); + MemberRenderer.renderPrivateSection(writer, shape, model); + }); + writer.write(""); + + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + private void renderSource(CppWriterDelegator writerDelegator, StructureShape shape) { + String className = shape.getId().getName(); + String fileName = "source/model/" + className + ".cpp"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + + if (protocol.isJsonLike()) { + writer.write("#include "); + } else { + writer.write("#include "); + } + writer.write("#include ", smithyServiceName, className); + writer.write(""); + writer.write("#include "); + writer.write(""); + + if (protocol.isJsonLike()) { + writer.write("using namespace Aws::Utils::Json;"); + writer.write("using namespace Aws::Utils;"); + } else { + writer.write("using namespace Aws::Utils::Xml;"); + writer.write("using namespace Aws::Utils;"); + } + writer.write(""); + + writer.writeNamespaceOpen("Aws"); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + writer.write(""); + + SerdeStub.renderSourceImplementation(writer, shape, model, protocol, + className, serviceName, smithyServiceName); + writer.write(""); + + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + private void renderForwardDeclarations(CppWriter writer) { + if (protocol.isJsonLike()) { + writer.writeNamespaceOpen("Utils"); + writer.writeNamespaceOpen("Json"); + writer.write("class JsonValue;"); + writer.write("class JsonView;"); + writer.writeNamespaceClose("Json"); + writer.writeNamespaceClose("Utils"); + } else { + writer.writeNamespaceOpen("Utils"); + writer.writeNamespaceOpen("Xml"); + writer.write("class XmlNode;"); + writer.writeNamespaceClose("Xml"); + writer.writeNamespaceClose("Utils"); + } + } + + private void renderClassDocComment(CppWriter writer, StructureShape shape) { + if (shape.getTrait(DocumentationTrait.class).isPresent()) { + String docText = MemberRenderer.collapseWhitespace( + shape.getTrait(DocumentationTrait.class).get().getValue()); + String version = service.getVersion(); + String seeAlso = String.format( + "

    See Also:

    AWS API Reference

    ", + smithyServiceName, version, shape.getId().getName()); + MemberRenderer.writeDocComment(writer, docText + seeAlso); + } else { + writer.write("/**"); + writer.write(" */"); + } + } + + private void writeCopyright(CppWriter writer) { + writer.write("/**"); + writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + writer.write(" * SPDX-License-Identifier: Apache-2.0."); + writer.write(" */"); + writer.write(""); + } +} From 0fa83b548ad87359b0d5f0bed41b0c1c18c0398e Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 21 Jul 2026 14:33:36 -0400 Subject: [PATCH 05/18] Wire TransformPipeline into ModelCodegenPlugin Transforms run before generation, per-service. Service-level transforms (S3, EC2, etc.) will implement ModelTransform and register in the pipeline. Also deduplicate sanitize() between ModelCodegenPlugin and ServiceNameUtil. --- .../generators/ServiceNameUtil.java | 2 +- .../generators/model/ModelCodegenPlugin.java | 21 ++++++++++++------- .../model/transforms/GlobalTransforms.java | 12 +++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java index 65dc089598e..2ba879102ba 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/ServiceNameUtil.java @@ -132,7 +132,7 @@ public static String capitalize(String str) { } // Match C2jModelToGeneratorModelTransformer.sanitizeServiceAbbreviation() exactly - private static String sanitizeServiceAbbreviation(String serviceAbbreviation) { + public static String sanitizeServiceAbbreviation(String serviceAbbreviation) { return serviceAbbreviation.replace(" ", "").replace("-", "").replace("_", "").replace("Amazon", "").replace("AWS", "").replace("/", ""); } 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 74da0f41a48..797e20b0975 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelCodegenPlugin.java @@ -6,6 +6,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; import com.amazonaws.util.awsclientsmithygenerator.generators.ServiceNameUtil; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.transforms.GlobalTransforms; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; import software.amazon.smithy.model.Model; @@ -14,6 +15,7 @@ import software.amazon.smithy.model.shapes.ServiceShape; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -38,12 +40,21 @@ public void execute(PluginContext context) { 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. + )); + CppWriterDelegator writerDelegator = new CppWriterDelegator(context.getFileManifest()); for (ServiceShape service : model.getServiceShapes()) { 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); @@ -52,7 +63,7 @@ public void execute(PluginContext context) { String namespace = namespaceMap.getOrDefault(smithyServiceName, serviceName); ModelGenerator generator = new ModelGenerator( - model, processedService, writerDelegator, + transformedModel, processedService, writerDelegator, serviceName, smithyServiceName, exportMacro, namespace); generator.generateAll(); } @@ -81,12 +92,8 @@ private Map parseNamespaceMap(ObjectNode settings) { .map(node -> node.getMembers().entrySet().stream() .collect(Collectors.toMap( entry -> entry.getKey().getValue(), - entry -> sanitize(entry.getValue().expectStringNode().getValue())))) + entry -> ServiceNameUtil.sanitizeServiceAbbreviation( + entry.getValue().expectStringNode().getValue())))) .orElse(Map.of()); } - - private static String sanitize(String s) { - return s.replace(" ", "").replace("-", "").replace("_", "") - .replace("Amazon", "").replace("AWS", "").replace("/", ""); - } } 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 055d95a8862..122946d678e 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.model.ModelTransform; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.ListShape; @@ -116,4 +117,15 @@ public static Set computeReachableShapes(Model model, ServiceShape serv } return reachable; } + + /** + * Returns this class as a no-op ModelTransform. + * + *

    GlobalTransforms currently operates at classification time (via computeReachableShapes), + * not at model-transform time. This factory exists to reserve a slot in the pipeline + * for future pre-generation model mutations (shape renaming, injection, etc.). + */ + public static ModelTransform asTransform() { + return (model, service) -> model; + } } From d24ead40e0170dd1d3814ad4bee83bad23055b77 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 21 Jul 2026 14:39:51 -0400 Subject: [PATCH 06/18] Deduplicate MemberRenderer public section methods Extract shared logic into renderMembers(emitHasBeenSet) to eliminate ~100 lines of near-identical code between request and result rendering. --- .../generators/model/MemberRenderer.java | 124 +++--------------- 1 file changed, 21 insertions(+), 103 deletions(-) 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 cc9074e1423..5b297e05052 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 @@ -50,108 +50,7 @@ private MemberRenderer() { */ public static void renderPublicSection(CppWriter writer, StructureShape shape, Model model, String exportMacro, String className) { - java.util.List> members = - new java.util.ArrayList<>(shape.getAllMembers().entrySet()); - for (int i = 0; i < members.size(); i++) { - Map.Entry entry = members.get(i); - String memberName = entry.getKey(); - MemberShape member = entry.getValue(); - Shape targetShape = model.expectShape(member.getTarget()); - String cppType = CppTypeMapper.getCppType(targetShape, model); - String fieldName = "m_" + decapitalize(memberName); - String templateParam = memberName + "T"; - - writer.write("///@{"); - - // Documentation comment - if (member.getTrait(DocumentationTrait.class).isPresent()) { - writeDocComment(writer, collapseWhitespace(member.getTrait(DocumentationTrait.class).get().getValue())); - } else { - writer.write(""); - } - - // Getter - if (isPrimitive(targetShape) || targetShape.isEnumShape()) { - writer.write("inline $L Get$L() const { return $L; }", cppType, memberName, fieldName); - } else { - writer.write("inline const $L& Get$L() const { return $L; }", cppType, memberName, fieldName); - } - - // HasBeenSet - writer.write("inline bool $LHasBeenSet() const { return $LHasBeenSet; }", memberName, fieldName); - - if (targetShape.isEnumShape() || isPrimitive(targetShape)) { - // Enum and primitive members use non-templated value setters - writer.openBlock("inline void Set$L($L value) {", "}", memberName, cppType, () -> { - writer.write("$LHasBeenSet = true;", fieldName); - writer.write("$L = value;", fieldName); - }); - - writer.openBlock("inline $L& With$L($L value) {", "}", className, memberName, cppType, () -> { - writer.write("Set$L(value);", memberName); - writer.write("return *this;"); - }); - } else { - // Templated Set - writer.write("template ", templateParam, cppType); - writer.openBlock("void Set$L($L&& value) {", "}", memberName, templateParam, () -> { - writer.write("$LHasBeenSet = true;", fieldName); - writer.write("$L = std::forward<$L>(value);", fieldName, templateParam); - }); - - // Templated With (fluent) - writer.write("template ", templateParam, cppType); - writer.openBlock("$L& With$L($L&& value) {", "}", className, memberName, templateParam, () -> { - writer.write("Set$L(std::forward<$L>(value));", memberName, templateParam); - writer.write("return *this;"); - }); - - // Add method for list types - if (targetShape.isListShape()) { - Shape elementShape = model.expectShape( - targetShape.asListShape().get().getMember().getTarget()); - String elementType = CppTypeMapper.getCppType(elementShape, model); - if (elementShape.isEnumShape()) { - writer.openBlock("inline $L& Add$L($L value) {", "}", className, memberName, elementType, () -> { - writer.write("$LHasBeenSet = true;", fieldName); - writer.write("$L.push_back(value);", fieldName); - writer.write("return *this;"); - }); - } else { - writer.write("template ", templateParam, elementType); - writer.openBlock("$L& Add$L($L&& value) {", "}", className, memberName, templateParam, () -> { - writer.write("$LHasBeenSet = true;", fieldName); - writer.write("$L.emplace_back(std::forward<$L>(value));", fieldName, templateParam); - writer.write("return *this;"); - }); - } - } - - // Add method for map types (two template params: key + value) - if (targetShape.isMapShape()) { - Shape keyShape = model.expectShape( - targetShape.asMapShape().get().getKey().getTarget()); - Shape valueShape = model.expectShape( - targetShape.asMapShape().get().getValue().getTarget()); - String keyType = CppTypeMapper.getCppType(keyShape, model); - String valueType = CppTypeMapper.getCppType(valueShape, model); - String keyParam = memberName + "KeyT"; - String valueParam = memberName + "ValueT"; - writer.write("template ", keyParam, keyType, valueParam, valueType); - writer.openBlock("$L& Add$L($L&& key, $L&& value) {", "}", className, memberName, keyParam, valueParam, () -> { - writer.write("$LHasBeenSet = true;", fieldName); - writer.write("$L.emplace(std::forward<$L>(key), std::forward<$L>(value));", fieldName, keyParam, valueParam); - writer.write("return *this;"); - }); - } - } - - writer.write("///@}"); - // Blank line between member blocks, but not after the last one - if (i < members.size() - 1) { - writer.write(""); - } - } + renderMembers(writer, shape, model, exportMacro, className, true); } /** @@ -165,6 +64,22 @@ public static void renderPublicSection(CppWriter writer, StructureShape shape, */ public static void renderPublicSectionForResult(CppWriter writer, StructureShape shape, Model model, String exportMacro, String className) { + renderMembers(writer, shape, model, exportMacro, className, false); + } + + /** + * Shared implementation for rendering public accessor methods. + * + * @param writer the CppWriter to write to + * @param shape the structure shape whose members to render + * @param model the model (for resolving member targets) + * @param exportMacro the export macro (e.g., "AWS_KINESIS_API") + * @param className the C++ class name (e.g., "ChildShard") + * @param emitHasBeenSet whether to emit HasBeenSet() accessor methods + */ + private static void renderMembers(CppWriter writer, StructureShape shape, + Model model, String exportMacro, String className, + boolean emitHasBeenSet) { java.util.List> members = new java.util.ArrayList<>(shape.getAllMembers().entrySet()); for (int i = 0; i < members.size(); i++) { @@ -191,7 +106,10 @@ public static void renderPublicSectionForResult(CppWriter writer, StructureShape writer.write("inline const $L& Get$L() const { return $L; }", cppType, memberName, fieldName); } - // No HasBeenSet for results + // HasBeenSet (only for non-result shapes) + if (emitHasBeenSet) { + writer.write("inline bool $LHasBeenSet() const { return $LHasBeenSet; }", memberName, fieldName); + } if (targetShape.isEnumShape() || isPrimitive(targetShape)) { writer.openBlock("inline void Set$L($L value) {", "}", memberName, cppType, () -> { From 60892d9b8312d15b2386fb19e7503c9dad1c2bbd Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 21 Jul 2026 14:41:52 -0400 Subject: [PATCH 07/18] Use FileManifest API in CppWriterDelegator Enables Smithy's built-in change detection and file tracking instead of raw Files.writeString(). --- .../generators/CppWriterDelegator.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java index 5253327247e..63b060d0bde 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java @@ -5,7 +5,6 @@ package com.amazonaws.util.awsclientsmithygenerator.generators; import software.amazon.smithy.build.FileManifest; -import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; @@ -25,15 +24,9 @@ public void useFileWriter(String filename, Consumer writerConsumer) { public void flushWriters() { writers.forEach((filename, writer) -> { - try { - Path outputPath = fileManifest.getBaseDir().resolve(filename); - java.nio.file.Files.createDirectories(outputPath.getParent()); - // Add UTF-8 BOM to match C2J-generated file format - String content = "" + writer.toString(); - java.nio.file.Files.writeString(outputPath, content); - } catch (Exception e) { - throw new RuntimeException("Failed to write file: " + filename, e); - } + // Add UTF-8 BOM to match C2J-generated file format + String content = "" + writer.toString(); + fileManifest.writeFile(filename, content); }); } } \ No newline at end of file From 423bcb733cc2bed80c151f55d2a9b1c4a3cc68a6 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Tue, 21 Jul 2026 14:44:15 -0400 Subject: [PATCH 08/18] Replace wildcard imports in ShapeClassifier with explicit imports --- .../generators/model/ShapeClassifier.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java index cef3712dc27..248a3544762 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ShapeClassifier.java @@ -8,10 +8,22 @@ 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.*; -import software.amazon.smithy.model.traits.*; - -import java.util.*; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +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.shapes.UnionShape; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.model.traits.StreamingTrait; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** From 2a767d4d8415a5c2e1735656e04dcb7d2b9e318e Mon Sep 17 00:00:00 2001 From: sbaluja Date: Mon, 27 Jul 2026 13:26:08 -0400 Subject: [PATCH 09/18] Add enum NOT_SET case --- .../generators/model/EnumRenderer.java | 8 +++++--- .../generators/model/EnumRendererTest.java | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) 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 67f414da4f5..0544261e7ed 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 @@ -151,8 +151,10 @@ public static void renderSource(CppWriter writer, Shape enumShape, String servic writer.write(""); // GetNameFor - writer.write(" Aws::String GetNameFor$1L($1L value) {", enumName); - writer.write(" switch (value) {"); + writer.write(" Aws::String GetNameFor$1L($1L enumValue) {", enumName); + writer.write(" switch (enumValue) {"); + writer.write(" case $1L::NOT_SET:", enumName); + writer.write(" return {};"); for (int i = 0; i < values.size(); i++) { writer.write(" case $1L::$2L:", enumName, values.get(i)); writer.write(" return \"$L\";", wireValues.get(i)); @@ -160,7 +162,7 @@ public static void renderSource(CppWriter writer, Shape enumShape, String servic writer.write(" default:"); writer.write(" EnumParseOverflowContainer* overflowContainer = Aws::GetEnumOverflowContainer();"); writer.write(" if (overflowContainer) {"); - writer.write(" return overflowContainer->RetrieveOverflow(static_cast(value));"); + writer.write(" return overflowContainer->RetrieveOverflow(static_cast(enumValue));"); writer.write(" }"); writer.write(" return {};"); writer.write(" }"); 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 23bf9fff966..806c523a5d1 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 @@ -116,8 +116,10 @@ void renderSource_producesGetNameForFunction() { CppWriter writer = new CppWriter(); EnumRenderer.renderSource(writer, enumShape, "Kinesis", "AWS_KINESIS_API", "kinesis"); String output = writer.toString(); - assertTrue(output.contains("Aws::String GetNameForStatus(Status value)"), + assertTrue(output.contains("Aws::String GetNameForStatus(Status enumValue)"), "Missing GetNameFor function: " + output); + assertTrue(output.contains("case Status::NOT_SET:"), + "Missing NOT_SET case: " + output); assertTrue(output.contains("case Status::CREATING:"), "Missing CREATING case: " + output); assertTrue(output.contains("case Status::ACTIVE:"), @@ -137,7 +139,7 @@ void renderSource_includesOverflowContainer() { "Missing overflow container in GetForName: " + output); assertTrue(output.contains("overflowContainer->StoreOverflow(hashCode, name)"), "Missing StoreOverflow: " + output); - assertTrue(output.contains("overflowContainer->RetrieveOverflow(static_cast(value))"), + assertTrue(output.contains("overflowContainer->RetrieveOverflow(static_cast(enumValue))"), "Missing RetrieveOverflow: " + output); } From de07f1cecdfab0a2084ae853688008ff8fa29c94 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 29 Jul 2026 13:45:44 -0400 Subject: [PATCH 10/18] Add event stream detection helpers to ShapeClassifier --- .../generators/model/ShapeClassifier.java | 34 +++++++++++ .../generators/model/ShapeClassifierTest.java | 57 +++++++++++++++++++ 2 files changed, 91 insertions(+) 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 248a3544762..df118be1ac0 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 @@ -192,4 +192,38 @@ private static boolean hasEventStreamMembers(StructureShape shape, Model model) return target.isUnionShape() && target.hasTrait(StreamingTrait.class); }); } + + /** + * Returns true if the operation's output structure has an event stream (a member + * targeting a {@code @streaming} union). This is the response-side (read) test. + * + * @param op the operation + * @param model the Smithy model + * @return true if the operation produces an event stream response + */ + public static boolean isEventStreamResponseOperation(OperationShape op, Model model) { + return op.getOutput() + .flatMap(model::getShape) + .flatMap(Shape::asStructureShape) + .map(out -> hasEventStreamMembers(out, model)) + .orElse(false); + } + + /** + * Returns true if the operation's input structure has an event stream (a member + * targeting a {@code @streaming} union). This is the request-side (write) test, + * used only to flag bidirectional requests; input-stream encoder generation is + * out of scope. + * + * @param op the operation + * @param model the Smithy model + * @return true if the operation consumes an event stream request + */ + public static boolean isEventStreamRequestOperation(OperationShape op, Model model) { + return op.getInput() + .flatMap(model::getShape) + .flatMap(Shape::asStructureShape) + .map(in -> hasEventStreamMembers(in, model)) + .orElse(false); + } } 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 dff69c02d9b..6fe308881af 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 @@ -246,6 +246,63 @@ void classifiesEventStreamHandler() { .anyMatch(r -> r.shape().getId().getName().equals("SubscribeRequest"))); } + private static Model modelWithStreaming(boolean inputStreams, boolean outputStreams) { + StringShape str = StringShape.builder().id("com.example#String").build(); + UnionShape eventStream = UnionShape.builder() + .id("com.example#EventStream") + .addTrait(new StreamingTrait()) + .addMember("event", str.getId()) + .build(); + + StructureShape.Builder inputBuilder = StructureShape.builder().id("com.example#OpInput"); + if (inputStreams) { + inputBuilder.addMember("body", eventStream.getId()); + } else { + inputBuilder.addMember("name", str.getId()); + } + StructureShape input = inputBuilder.build(); + + StructureShape.Builder outputBuilder = StructureShape.builder().id("com.example#OpOutput"); + if (outputStreams) { + outputBuilder.addMember("stream", eventStream.getId()); + } else { + outputBuilder.addMember("result", str.getId()); + } + StructureShape output = outputBuilder.build(); + + OperationShape op = OperationShape.builder() + .id("com.example#Op") + .input(input.getId()) + .output(output.getId()) + .build(); + + return Model.builder().addShapes(str, eventStream, input, output, op).build(); + } + + @Test + void isEventStreamResponseOperation_trueWhenOutputStreams() { + Model model = modelWithStreaming(false, true); + OperationShape op = model.expectShape(ShapeId.from("com.example#Op"), OperationShape.class); + assertTrue(ShapeClassifier.isEventStreamResponseOperation(op, model)); + assertFalse(ShapeClassifier.isEventStreamRequestOperation(op, model)); + } + + @Test + void isEventStreamRequestOperation_trueWhenInputStreams() { + Model model = modelWithStreaming(true, true); + OperationShape op = model.expectShape(ShapeId.from("com.example#Op"), OperationShape.class); + assertTrue(ShapeClassifier.isEventStreamRequestOperation(op, model)); + assertTrue(ShapeClassifier.isEventStreamResponseOperation(op, model)); + } + + @Test + void isEventStreamResponseOperation_falseWhenNeitherStreams() { + Model model = modelWithStreaming(false, false); + OperationShape op = model.expectShape(ShapeId.from("com.example#Op"), OperationShape.class); + assertFalse(ShapeClassifier.isEventStreamResponseOperation(op, model)); + assertFalse(ShapeClassifier.isEventStreamRequestOperation(op, model)); + } + @Test void classifiesEnumShape() { // StringShape with @enum trait -> classified as enum From 97bbfd6edeebf860322b0f62d20f3982580c899f Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 29 Jul 2026 13:48:33 -0400 Subject: [PATCH 11/18] Add event-stream serde stub helpers to SerdeStub --- .../generators/model/SerdeStub.java | 40 +++++++++++++++ .../generators/model/SerdeStubTest.java | 49 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStubTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java index d17bdf26de2..d383740f107 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java @@ -56,6 +56,46 @@ public static void renderSourceImplementation(CppWriter writer, StructureShape s } } + /** + * Emits the empty per-protocol serde source bodies for a class that has no backing + * StructureShape (event stream union / initial response). Mirrors + * {@link #renderSourceImplementation} but keyed only on the class name. + */ + public static void renderSerdeSourceStub(CppWriter writer, Protocol protocol, String className) { + if (protocol.isJsonLike()) { + renderJsonStub(writer, className); + } else if (protocol == Protocol.REST_XML) { + renderXmlStub(writer, className); + } else if (protocol == Protocol.QUERY_XML || protocol == Protocol.EC2) { + renderQueryXmlStub(writer, className); + } else { + throw new UnsupportedOperationException("Unsupported protocol for serde source stub: " + protocol); + } + } + + /** + * Emits a protocol-agnostic stub for an event-stream event case body: a TODO marker + * and a minimal, compilable callback invocation with a default-constructed event. + * No protocol-specific deserialization is emitted. + * + * @param writer the CppWriter + * @param eventShapeName the C++ event shape type (e.g. "SubscribeToShardEvent") + * @param callbackMember the handler callback member (e.g. "m_onSubscribeToShardEvent") + */ + public static void renderEventPayloadDecodeStub(CppWriter writer, String eventShapeName, + String callbackMember) { + writer.write("// TODO: protocol-specific event payload deserialization"); + writer.write("$L($L{});", callbackMember, eventShapeName); + } + + /** + * Emits a protocol-agnostic stub in place of the protocol-specific error-payload parse. + * The header-derived error code/message still flow to MarshallError at the call site. + */ + public static void renderErrorPayloadParseStub(CppWriter writer) { + writer.write("// TODO: protocol-specific error payload deserialization"); + } + private static void renderJsonStub(CppWriter writer, String className) { writer.write("$L::$L(JsonView jsonValue) { *this = jsonValue; }", className, className); writer.write(""); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStubTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStubTest.java new file mode 100644 index 00000000000..2335c427e3a --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStubTest.java @@ -0,0 +1,49 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package com.amazonaws.util.awsclientsmithygenerator.generators.model; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SerdeStubTest { + + @Test + void renderEventPayloadDecodeStub_emitsTodoAndNoProtocolTokens() { + CppWriter writer = new CppWriter(); + SerdeStub.renderEventPayloadDecodeStub(writer, "SubscribeToShardEvent", "m_onSubscribeToShardEvent"); + String out = writer.toString(); + assertTrue(out.contains("// TODO: protocol-specific event payload deserialization"), + "Missing TODO marker: " + out); + assertTrue(out.contains("m_onSubscribeToShardEvent(SubscribeToShardEvent{});"), + "Missing callback invocation: " + out); + assertFalse(out.contains("JsonValue"), "Should not emit JSON tokens: " + out); + assertFalse(out.contains("Cbor"), "Should not emit CBOR tokens: " + out); + } + + @Test + void renderErrorPayloadParseStub_emitsTodoOnly() { + CppWriter writer = new CppWriter(); + SerdeStub.renderErrorPayloadParseStub(writer); + String out = writer.toString(); + assertTrue(out.contains("// TODO: protocol-specific error payload deserialization"), + "Missing TODO marker: " + out); + assertFalse(out.contains("JsonView"), "Should not emit JSON tokens: " + out); + } + + @Test + void renderSerdeSourceStub_json_emitsEmptyBodies() { + CppWriter writer = new CppWriter(); + SerdeStub.renderSerdeSourceStub(writer, Protocol.JSON, "SubscribeToShardEventStream"); + String out = writer.toString(); + assertTrue(out.contains("SubscribeToShardEventStream::SubscribeToShardEventStream(JsonView jsonValue)"), + "Missing JSON ctor stub: " + out); + assertTrue(out.contains("JsonValue SubscribeToShardEventStream::Jsonize() const"), + "Missing Jsonize stub: " + out); + } +} From 83fca43a1a58fee9ac5ce25e17dfb648aa5d48ea Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 29 Jul 2026 13:56:17 -0400 Subject: [PATCH 12/18] Add EventStreamRenderer with handler generation --- .../generators/model/ModelGenerator.java | 6 +- .../model/renderers/EventStreamRenderer.java | 417 ++++++++++++++++++ .../model/EventStreamRendererTest.java | 119 +++++ 3 files changed, 540 insertions(+), 2 deletions(-) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java index 15ba3057476..b9bc248e29f 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/ModelGenerator.java @@ -8,6 +8,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.ClassifiedShapes; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EnumShapeRenderer; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.EventStreamRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.RequestRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.ResultRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.renderers.SubObjectRenderer; @@ -74,8 +75,9 @@ private List buildRenderers(ClassifiedShapes classified) { classified.results(), model, service, protocol, namespace, exportMacro, smithyServiceName)); - // Future: event stream renderers will be registered here - // renderers.add(new EventStreamRenderer(...)); + renderers.add(new EventStreamRenderer( + classified.eventStreamHandlers(), model, service, protocol, + namespace, exportMacro, smithyServiceName)); return renderers; } 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 new file mode 100644 index 00000000000..5d0e16fce88 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.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.renderers; + +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; +import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.SerdeStub; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.EventStreamInfo; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeRenderer; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.model.traits.StreamingTrait; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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 SerdeStub}. + */ +public final class EventStreamRenderer implements ShapeRenderer { + + private final List eventStreams; + private final Model model; + private final ServiceShape service; + private final Protocol protocol; + private final String namespace; + private final String exportMacro; + private final String smithyServiceName; + + public EventStreamRenderer(List eventStreams, Model model, ServiceShape service, + Protocol protocol, String namespace, String exportMacro, + String smithyServiceName) { + this.eventStreams = eventStreams; + this.model = model; + this.service = service; + this.protocol = protocol; + this.namespace = namespace; + this.exportMacro = exportMacro; + this.smithyServiceName = smithyServiceName; + } + + @Override + public void render(CppWriterDelegator writerDelegator) { + for (EventStreamInfo info : eventStreams) { + UnionShape union = findStreamingUnion(info.resultShape()); + if (union == null) { + continue; + } + List events = new ArrayList<>(); + List exceptions = new ArrayList<>(); + partitionMembers(union, events, exceptions); + + renderHandlerHeader(writerDelegator, info.operationName(), events); + renderHandlerSource(writerDelegator, info.operationName(), events); + renderInitialResponse(writerDelegator, info.operationName()); + renderEventStreamUnion(writerDelegator, info.operationName(), union, events, exceptions); + } + } + + /** Finds the @streaming union targeted by a member of the result structure. */ + private UnionShape findStreamingUnion(StructureShape resultShape) { + for (MemberShape member : resultShape.getAllMembers().values()) { + Shape target = model.expectShape(member.getTarget()); + if (target.isUnionShape() && target.hasTrait(StreamingTrait.class)) { + return target.asUnionShape().get(); + } + } + return null; + } + + /** Splits union members into events (non-exception) and exceptions, preserving order. */ + private void partitionMembers(UnionShape union, List events, List exceptions) { + for (MemberShape member : union.getAllMembers().values()) { + Shape target = model.expectShape(member.getTarget()); + if (target.hasTrait(ErrorTrait.class)) { + exceptions.add(member); + } else { + events.add(member); + } + } + } + + /** The C++ event type identifier (event shape name), e.g. "AlphaEvent". */ + private String eventShapeName(MemberShape member) { + return member.getTarget().getName(); + } + + /** The wire member key, e.g. "alpha". */ + private String wireKey(MemberShape member) { + return member.getMemberName(); + } + + /** The EventType enum constant: wire member key uppercased, e.g. "ALPHA". */ + private String enumConstant(MemberShape member) { + return wireKey(member).toUpperCase(); + } + + // ---- Handler ------------------------------------------------------------ + + private void renderHandlerHeader(CppWriterDelegator writerDelegator, String opName, + List events) { + String className = opName + "Handler"; + String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + writer.write("#include "); + writer.write("#include "); + writer.write("#include "); + writer.write("#include ", smithyServiceName, namespace); + writer.write("#include ", smithyServiceName, namespace); + writer.write("#include ", smithyServiceName, opName); + for (MemberShape event : events) { + writer.write("#include ", smithyServiceName, eventShapeName(event)); + } + writer.write(""); + writer.writeNamespaceOpen("Aws"); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + + // EventType enum + StringBuilder enumBody = new StringBuilder("enum class ") + .append(opName).append("EventType { INITIAL_RESPONSE, "); + for (MemberShape event : events) { + enumBody.append(enumConstant(event)).append(", "); + } + enumBody.append("UNKNOWN };"); + writer.write("$L", enumBody.toString()); + 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); + } + writer.write("typedef std::function& error)> ErrorCallback;", namespace); + writer.write(""); + writer.dedent(); + writer.write("public:"); + writer.indent(); + writer.write("$1L $2L();", exportMacro, className); + writer.write("$1L $2L& operator=(const $2L&) = default;", exportMacro, className); + writer.write("$1L $2L(const $2L&) = default;", exportMacro, className); + writer.write(""); + writer.write("$1L virtual void OnEvent() override;", exportMacro); + writer.write(""); + writer.write("inline void SetInitialResponseCallbackEx(const $1LInitialResponseCallbackEx& callback) { m_onInitialResponse = callback; }", opName); + writer.openBlock("inline void SetInitialResponseCallback(const $1LInitialResponseCallback& noArgCallback) {", "}", opName, () -> { + writer.write("m_onInitialResponse = [noArgCallback](const $1LInitialResponse& rs, const Utils::Event::InitialResponseType) {", opName); + writer.write("return noArgCallback(rs);"); + writer.write("};"); + }); + for (MemberShape event : events) { + String ev = eventShapeName(event); + writer.write("inline void Set$1LCallback(const $1LCallback& callback) { m_on$1L = callback; }", ev); + } + writer.write("inline void SetOnErrorCallback(const ErrorCallback& callback) { m_onError = callback; }"); + writer.write(""); + writer.write("inline $1LInitialResponseCallbackEx& GetInitialResponseCallbackEx() { return m_onInitialResponse; }", opName); + writer.write(""); + writer.dedent(); + writer.write("private:"); + writer.indent(); + writer.write("$1L void HandleEventInMessage();", exportMacro); + writer.write("$1L void HandleErrorInMessage();", exportMacro); + writer.write("$1L void MarshallError(const Aws::String& errorCode, const Aws::String& errorMessage);", exportMacro); + writer.write(""); + writer.write("$1LInitialResponseCallbackEx m_onInitialResponse;", opName); + for (MemberShape event : events) { + String ev = eventShapeName(event); + writer.write("$1LCallback m_on$1L;", ev); + } + writer.write("ErrorCallback m_onError;"); + }); + writer.write(""); + + writer.writeNamespaceOpen(opName + "EventMapper"); + writer.write("$1L $2LEventType Get$2LEventTypeForName(const Aws::String& name);", exportMacro, opName); + writer.write(""); + writer.write("$1L Aws::String GetNameFor$2LEventType($2LEventType value);", exportMacro, opName); + writer.writeNamespaceClose(opName + "EventMapper"); + + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + private void renderHandlerSource(CppWriterDelegator writerDelegator, String opName, + List events) { + String className = opName + "Handler"; + String fileName = "source/model/" + className + ".cpp"; + String tag = opName.toUpperCase() + "_HANDLER_CLASS_TAG"; + writerDelegator.useFileWriter(fileName, writer -> { + writeCopyright(writer); + writer.write("#include "); + writer.write("#include "); + writer.write("#include "); + writer.write("#include ", smithyServiceName, namespace); + writer.write("#include ", smithyServiceName, opName); + writer.write(""); + writer.write("using namespace Aws::$1L::Model;", namespace); + writer.write("using namespace Aws::Utils::Event;"); + writer.write(""); + writer.write("AWS_CORE_API extern const char MESSAGE_LOWER_CASE[];"); + writer.write("AWS_CORE_API extern const char MESSAGE_CAMEL_CASE[];"); + writer.write(""); + writer.writeNamespaceOpen("Aws"); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + writer.write("using namespace Aws::Client;"); + writer.write(""); + 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); + }); + } + writer.openBlock("m_onError = [&](const AWSError<$1LErrors>& error) {", "};", namespace, () -> { + writer.write("AWS_LOGSTREAM_TRACE($1L, \"$2L Errors received, \" << error);", tag, namespace); + }); + }); + writer.write(""); + + // OnEvent + writer.openBlock("void $1L::OnEvent() {", "}", className, () -> { + writer.openBlock("if (!*this) {", "}", () -> { + writer.write("AWSError error = EventStreamErrorsMapper::GetAwsErrorForEventStreamError(GetInternalError());"); + writer.write("error.SetMessage(GetEventPayloadAsString());"); + writer.write("m_onError(AWSError<$1LErrors>(error));", namespace); + writer.write("return;"); + }); + writer.write("const auto& headers = GetEventHeaders();"); + writer.write("auto messageTypeHeaderIter = headers.find(MESSAGE_TYPE_HEADER);"); + writer.openBlock("if (messageTypeHeaderIter == headers.end()) {", "}", () -> { + writer.write("AWS_LOGSTREAM_WARN($1L, \"Header: \" << MESSAGE_TYPE_HEADER << \" not found in the message.\");", tag); + writer.write("return;"); + }); + writer.openBlock("switch (Aws::Utils::Event::Message::GetMessageTypeForName(messageTypeHeaderIter->second.GetEventHeaderValueAsString())) {", "}", () -> { + writer.write("case Aws::Utils::Event::Message::MessageType::EVENT:"); + writer.write("HandleEventInMessage();"); + writer.write("break;"); + writer.write("case Aws::Utils::Event::Message::MessageType::REQUEST_LEVEL_ERROR:"); + writer.openBlock("case Aws::Utils::Event::Message::MessageType::REQUEST_LEVEL_EXCEPTION: {", "}", () -> { + writer.write("HandleErrorInMessage();"); + writer.write("break;"); + }); + writer.write("default:"); + writer.write("AWS_LOGSTREAM_WARN($1L, \"Unexpected message type: \" << messageTypeHeaderIter->second.GetEventHeaderValueAsString());", tag); + writer.write("break;"); + }); + }); + writer.write(""); + + // HandleEventInMessage + writer.openBlock("void $1L::HandleEventInMessage() {", "}", className, () -> { + writer.write("const auto& headers = GetEventHeaders();"); + writer.write("auto eventTypeHeaderIter = headers.find(EVENT_TYPE_HEADER);"); + writer.openBlock("if (eventTypeHeaderIter == headers.end()) {", "}", () -> { + writer.write("AWS_LOGSTREAM_WARN($1L, \"Header: \" << EVENT_TYPE_HEADER << \" not found in the message.\");", tag); + writer.write("return;"); + }); + writer.openBlock("switch ($1LEventMapper::Get$1LEventTypeForName(eventTypeHeaderIter->second.GetEventHeaderValueAsString())) {", "}", opName, () -> { + writer.openBlock("case $1LEventType::INITIAL_RESPONSE: {", "}", opName, () -> { + writer.write("$1LInitialResponse event(GetEventHeadersAsHttpHeaders());", opName); + writer.write("m_onInitialResponse(event, Utils::Event::InitialResponseType::ON_EVENT);"); + writer.write("break;"); + }); + for (MemberShape event : events) { + writer.openBlock("case $1LEventType::$2L: {", "}", opName, enumConstant(event), () -> { + SerdeStub.renderEventPayloadDecodeStub(writer, eventShapeName(event), "m_on" + eventShapeName(event)); + writer.write("break;"); + }); + } + writer.write("default:"); + writer.write("AWS_LOGSTREAM_WARN($1L, \"Unexpected event type: \" << eventTypeHeaderIter->second.GetEventHeaderValueAsString());", tag); + writer.write("break;"); + }); + }); + writer.write(""); + + // HandleErrorInMessage + writer.openBlock("void $1L::HandleErrorInMessage() {", "}", className, () -> { + writer.write("const auto& headers = GetEventHeaders();"); + writer.write("Aws::String errorCode;"); + writer.write("Aws::String errorMessage;"); + writer.write("auto errorHeaderIter = headers.find(ERROR_CODE_HEADER);"); + writer.openBlock("if (errorHeaderIter == headers.end()) {", "}", () -> { + writer.write("errorHeaderIter = headers.find(EXCEPTION_TYPE_HEADER);"); + writer.openBlock("if (errorHeaderIter == headers.end()) {", "}", () -> { + writer.write("AWS_LOGSTREAM_WARN($1L, \"Error type was not found in the event message.\");", tag); + writer.write("return;"); + }); + }); + writer.write("errorCode = errorHeaderIter->second.GetEventHeaderValueAsString();"); + writer.write("errorHeaderIter = headers.find(ERROR_MESSAGE_HEADER);"); + writer.openBlock("if (errorHeaderIter == headers.end()) {", "}", () -> { + writer.write("// TODO: read error message from payload once protocol-specific serde lands"); + SerdeStub.renderErrorPayloadParseStub(writer); + }); + writer.openBlock("else {", "}", () -> { + writer.write("errorMessage = errorHeaderIter->second.GetEventHeaderValueAsString();"); + }); + writer.write("MarshallError(errorCode, errorMessage);"); + }); + writer.write(""); + + // MarshallError (verbatim from C2J) + writer.openBlock("void $1L::MarshallError(const Aws::String& errorCode, const Aws::String& errorMessage) {", "}", className, () -> { + writer.write("$1LErrorMarshaller errorMarshaller;", namespace); + writer.write("AWSError error;"); + writer.openBlock("if (errorCode.empty()) {", "}", () -> { + writer.write("error = AWSError(CoreErrors::UNKNOWN, \"\", errorMessage, false);"); + }); + writer.openBlock("else {", "}", () -> { + writer.write("error = errorMarshaller.FindErrorByName(errorCode.c_str());"); + writer.openBlock("if (error.GetErrorType() != CoreErrors::UNKNOWN) {", "}", () -> { + writer.write("AWS_LOGSTREAM_WARN($1L, \"Encountered AWSError '\" << errorCode.c_str() << \"': \" << errorMessage.c_str());", tag); + writer.write("error.SetExceptionName(errorCode);"); + writer.write("error.SetMessage(errorMessage);"); + }); + writer.openBlock("else {", "}", () -> { + writer.write("AWS_LOGSTREAM_WARN($1L, \"Encountered Unknown AWSError '\" << errorCode.c_str() << \"': \" << errorMessage.c_str());", tag); + writer.write("error = AWSError(CoreErrors::UNKNOWN, errorCode, \"Unable to parse ExceptionName: \" + errorCode + \" Message: \" + errorMessage, false);"); + }); + }); + writer.write("m_onError(AWSError<$1LErrors>(error));", namespace); + }); + 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) { + writer.write("static const int $1L_HASH = Aws::Utils::HashingUtils::HashString(\"$2L\");", + enumConstant(event), wireKey(event)); + } + writer.write(""); + writer.openBlock("$1LEventType Get$1LEventTypeForName(const Aws::String& name) {", "}", opName, () -> { + writer.write("int hashCode = Aws::Utils::HashingUtils::HashString(name.c_str());"); + writer.openBlock("if (hashCode == INITIAL_RESPONSE_HASH) {", "}", () -> { + writer.write("return $1LEventType::INITIAL_RESPONSE;", opName); + }); + for (MemberShape event : events) { + writer.openBlock("else if (hashCode == $1L_HASH) {", "}", enumConstant(event), () -> { + writer.write("return $1LEventType::$2L;", opName, enumConstant(event)); + }); + } + writer.write("return $1LEventType::UNKNOWN;", opName); + }); + writer.write(""); + writer.openBlock("Aws::String GetNameFor$1LEventType($1LEventType value) {", "}", opName, () -> { + writer.openBlock("switch (value) {", "}", () -> { + writer.write("case $1LEventType::INITIAL_RESPONSE:", opName); + writer.write("return \"initial-response\";"); + for (MemberShape event : events) { + writer.write("case $1LEventType::$2L:", opName, enumConstant(event)); + writer.write("return \"$1L\";", wireKey(event)); + } + writer.write("default:"); + writer.write("return \"Unknown\";"); + }); + }); + writer.writeNamespaceClose(opName + "EventMapper"); + + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + } + + // ---- Placeholders filled in by Tasks 4 and 5 ---------------------------- + + private void renderInitialResponse(CppWriterDelegator writerDelegator, String opName) { + // Implemented in Task 4. + } + + private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String opName, + UnionShape union, List events, + List exceptions) { + // Implemented in Task 5. + } + + private void writeCopyright(CppWriter writer) { + writer.write("/**"); + writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + writer.write(" * SPDX-License-Identifier: Apache-2.0."); + writer.write(" */"); + writer.write(""); + } +} diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java new file mode 100644 index 00000000000..7d570a6080d --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/EventStreamRendererTest.java @@ -0,0 +1,119 @@ +/** + * 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.renderers.EventStreamRenderer; +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 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 software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.model.traits.StreamingTrait; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class EventStreamRendererTest { + + private static Model twoEventModel() { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape eventA = StructureShape.builder() + .id("com.example#AlphaEvent") + .addMember("data", str.getId()) + .build(); + StructureShape eventB = StructureShape.builder() + .id("com.example#BetaEvent") + .addMember("data", str.getId()) + .build(); + StructureShape exc = StructureShape.builder() + .id("com.example#BadException") + .addTrait(new ErrorTrait("client")) + .addMember("message", str.getId()) + .build(); + UnionShape stream = UnionShape.builder() + .id("com.example#MyStream") + .addTrait(new StreamingTrait()) + .addMember("alpha", eventA.getId()) + .addMember("beta", eventB.getId()) + .addMember("badException", exc.getId()) + .build(); + StructureShape input = StructureShape.builder() + .id("com.example#DoStreamInput") + .addMember("name", str.getId()) + .build(); + StructureShape output = StructureShape.builder() + .id("com.example#DoStreamOutput") + .addMember("stream", stream.getId()) + .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, eventA, eventB, exc, input, output, op, service).build(); + } + + private static String render(String fileSuffix) { + Model model = twoEventModel(); + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + EventStreamRenderer renderer = new EventStreamRenderer( + ShapeClassifier.classify(model, service).eventStreamHandlers(), + model, service, ProtocolResolver.resolve(service, model), + "Example", "AWS_EXAMPLE_API", "example"); + renderer.render(delegator); + delegator.flushWriters(); + return manifest.getFileString( + manifest.getFiles().stream() + .filter(p -> p.toString().endsWith(fileSuffix)) + .findFirst().orElseThrow()) + .orElseThrow(); + } + + @Test + void handlerHeader_hasEnumAndCallbacksPerEvent() { + String h = render("DoStreamHandler.h"); + assertTrue(h.contains("enum class DoStreamEventType"), "Missing EventType enum: " + h); + assertTrue(h.contains("INITIAL_RESPONSE"), "Missing INITIAL_RESPONSE: " + h); + assertTrue(h.contains("ALPHA") && h.contains("BETA"), "Missing event enum constants: " + h); + assertTrue(h.contains("UNKNOWN"), "Missing UNKNOWN: " + h); + assertTrue(h.contains("SetAlphaEventCallback") && h.contains("SetBetaEventCallback"), + "Missing per-event setters: " + h); + assertTrue(h.contains("m_onAlphaEvent") && h.contains("m_onBetaEvent"), + "Missing per-event members: " + h); + assertTrue(h.contains("void OnEvent() override"), "Missing OnEvent override: " + h); + // Exception must NOT get its own callback + assertFalse(h.contains("SetBadExceptionCallback"), "Exception should not get a callback: " + h); + } + + @Test + void handlerSource_eventCasesUseStubNoProtocolTokens() { + String c = render("DoStreamHandler.cpp"); + assertTrue(c.contains("// TODO: protocol-specific event payload deserialization"), + "Missing event decode stub: " + c); + assertTrue(c.contains("m_onAlphaEvent(AlphaEvent{});"), "Missing alpha stub body: " + c); + assertTrue(c.contains("case DoStreamEventType::INITIAL_RESPONSE:"), + "Missing INITIAL_RESPONSE case: " + c); + assertTrue(c.contains("// TODO: protocol-specific error payload deserialization"), + "Missing error parse stub: " + c); + // EventMapper hash uses the wire member key, not the shape name + assertTrue(c.contains("HashString(\"alpha\")") && c.contains("HashString(\"beta\")"), + "EventMapper must key on wire member name: " + c); + assertFalse(c.contains("JsonValue"), "No protocol tokens in handler: " + c); + assertFalse(c.contains("Cbor"), "No protocol tokens in handler: " + c); + } +} From 3839e90c5b3dbd77c958c2196694449ddbd40c63 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 29 Jul 2026 13:59:24 -0400 Subject: [PATCH 13/18] Add InitialResponse generation to EventStreamRenderer --- .../model/renderers/EventStreamRenderer.java | 69 ++++++++++++++++++- .../model/EventStreamRendererTest.java | 18 +++++ 2 files changed, 86 insertions(+), 1 deletion(-) 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 5d0e16fce88..f2453c9cd0c 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 @@ -398,7 +398,74 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa // ---- Placeholders filled in by Tasks 4 and 5 ---------------------------- private void renderInitialResponse(CppWriterDelegator writerDelegator, String opName) { - // Implemented in Task 4. + String className = opName + "InitialResponse"; + + String headerFile = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(headerFile, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + writer.write("#include "); + writer.write("#include ", smithyServiceName, namespace); + writer.write(""); + writer.writeNamespaceOpen("Aws"); + renderSerdeForwardDeclarations(writer); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + writer.write(""); + writer.openBlock("class $L {", "};", className, () -> { + writer.write("public:"); + SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className); + writer.write("$1L $2L(const Http::HeaderValueCollection& responseHeaders);", exportMacro, className); + }); + writer.write(""); + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + + String sourceFile = "source/model/" + className + ".cpp"; + writerDelegator.useFileWriter(sourceFile, writer -> { + writeCopyright(writer); + if (protocol.isJsonLike()) { + writer.write("#include "); + } else { + writer.write("#include "); + } + writer.write("#include "); + writer.write("#include ", smithyServiceName, className); + writer.write(""); + writer.write("using namespace Aws::$1L::Model;", namespace); + if (protocol.isJsonLike()) { + writer.write("using namespace Aws::Utils::Json;"); + } else { + writer.write("using namespace Aws::Utils::Xml;"); + } + writer.write("using namespace Aws::Utils;"); + writer.write(""); + SerdeStub.renderSerdeSourceStub(writer, protocol, className); + writer.write(""); + writer.openBlock("$1L::$1L(const Http::HeaderValueCollection& responseHeaders) {", "}", className, () -> { + writer.write("AWS_UNREFERENCED_PARAM(responseHeaders);"); + }); + }); + } + + /** Forward-declares the protocol serde types under Aws:: for header use. */ + private void renderSerdeForwardDeclarations(CppWriter writer) { + if (protocol.isJsonLike()) { + writer.writeNamespaceOpen("Utils"); + writer.writeNamespaceOpen("Json"); + writer.write("class JsonValue;"); + writer.write("class JsonView;"); + writer.writeNamespaceClose("Json"); + writer.writeNamespaceClose("Utils"); + } else { + writer.writeNamespaceOpen("Utils"); + writer.writeNamespaceOpen("Xml"); + writer.write("class XmlNode;"); + writer.writeNamespaceClose("Xml"); + writer.writeNamespaceClose("Utils"); + } } private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String opName, 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 7d570a6080d..17fe21f163e 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 @@ -100,6 +100,24 @@ void handlerHeader_hasEnumAndCallbacksPerEvent() { assertFalse(h.contains("SetBadExceptionCallback"), "Exception should not get a callback: " + h); } + @Test + void initialResponseHeader_hasHeaderCollectionCtorAndSerdeDecls() { + String h = render("DoStreamInitialResponse.h"); + assertTrue(h.contains("class DoStreamInitialResponse"), "Missing class: " + h); + assertTrue(h.contains("DoStreamInitialResponse(const Http::HeaderValueCollection& responseHeaders)"), + "Missing header-collection ctor: " + h); + // JSON serde declaration present (test model resolves to a JSON-like protocol) + assertTrue(h.contains("Jsonize") || h.contains("XmlNode") || h.contains("OutputToStream"), + "Missing serde declarations: " + h); + } + + @Test + void initialResponseSource_hasHeaderCtorAndSerdeStub() { + String c = render("DoStreamInitialResponse.cpp"); + assertTrue(c.contains("DoStreamInitialResponse::DoStreamInitialResponse(const Http::HeaderValueCollection&"), + "Missing header ctor definition: " + c); + } + @Test void handlerSource_eventCasesUseStubNoProtocolTokens() { String c = render("DoStreamHandler.cpp"); From e037dc91f935d51e930d66d46d35bd202af4db50 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 29 Jul 2026 14:04:15 -0400 Subject: [PATCH 14/18] Add EventStream union generation to EventStreamRenderer --- .../model/renderers/EventStreamRenderer.java | 114 +++++++++++++++++- .../model/EventStreamRendererTest.java | 14 ++- 2 files changed, 126 insertions(+), 2 deletions(-) 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 f2453c9cd0c..ea7bb847559 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 @@ -471,7 +471,119 @@ private void renderSerdeForwardDeclarations(CppWriter writer) { private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String opName, UnionShape union, List events, List exceptions) { - // Implemented in Task 5. + String className = union.getId().getName(); + String errorType = namespace + "Error"; + + String headerFile = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; + writerDelegator.useFileWriter(headerFile, writer -> { + writeCopyright(writer); + writer.write("#pragma once"); + writer.write("#include ", smithyServiceName, namespace); + writer.write("#include ", smithyServiceName, namespace); + for (MemberShape event : events) { + writer.write("#include ", smithyServiceName, eventShapeName(event)); + } + writer.write(""); + writer.write("#include "); + writer.write(""); + writer.writeNamespaceOpen("Aws"); + renderSerdeForwardDeclarations(writer); + writer.writeNamespaceOpen(namespace); + writer.writeNamespaceOpen("Model"); + writer.write(""); + writer.openBlock("class $L {", "};", className, () -> { + writer.write("public:"); + SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className); + writer.write(""); + // Event member accessors, typed as their concrete shape. + for (MemberShape event : events) { + String cppType = CppTypeMapper.getCppType(model.expectShape(event.getTarget()), model); + renderShapeAccessor(writer, className, cppType, event.getMemberName()); + } + // Exception member accessors, typed as Error. + for (MemberShape exc : exceptions) { + renderShapeAccessor(writer, className, errorType, exc.getMemberName()); + } + writer.dedent(); + writer.write("private:"); + writer.indent(); + // Data members + for (MemberShape event : events) { + String cppType = CppTypeMapper.getCppType(model.expectShape(event.getTarget()), model); + writer.write("$1L $2L;", cppType, "m_" + decapitalize(event.getMemberName())); + } + for (MemberShape exc : exceptions) { + writer.write("$1L $2L;", errorType, "m_" + decapitalize(exc.getMemberName())); + } + // HasBeenSet flags + for (MemberShape event : events) { + writer.write("bool $1LHasBeenSet = false;", "m_" + decapitalize(event.getMemberName())); + } + for (MemberShape exc : exceptions) { + writer.write("bool $1LHasBeenSet = false;", "m_" + decapitalize(exc.getMemberName())); + } + }); + writer.write(""); + writer.writeNamespaceClose("Model"); + writer.writeNamespaceClose(namespace); + writer.writeNamespaceClose("Aws"); + }); + + String sourceFile = "source/model/" + className + ".cpp"; + writerDelegator.useFileWriter(sourceFile, writer -> { + writeCopyright(writer); + if (protocol.isJsonLike()) { + writer.write("#include "); + } else { + writer.write("#include "); + } + writer.write("#include ", smithyServiceName, className); + writer.write(""); + writer.write("using namespace Aws::$1L::Model;", namespace); + if (protocol.isJsonLike()) { + writer.write("using namespace Aws::Utils::Json;"); + } else { + writer.write("using namespace Aws::Utils::Xml;"); + } + writer.write("using namespace Aws::Utils;"); + writer.write(""); + SerdeStub.renderSerdeSourceStub(writer, protocol, className); + }); + } + + /** + * 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) { + String getter = capitalize(memberName); + String field = "m_" + decapitalize(memberName); + String templateParam = getter + "T"; + writer.write("///@{"); + 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("///@}"); + } + + private static String capitalize(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + private static String decapitalize(String s) { + return s.substring(0, 1).toLowerCase() + s.substring(1); } private void writeCopyright(CppWriter writer) { 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 17fe21f163e..225bb0b6ab9 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 @@ -38,7 +38,7 @@ private static Model twoEventModel() { .addMember("message", str.getId()) .build(); UnionShape stream = UnionShape.builder() - .id("com.example#MyStream") + .id("com.example#MyStreamEventStream") .addTrait(new StreamingTrait()) .addMember("alpha", eventA.getId()) .addMember("beta", eventB.getId()) @@ -100,6 +100,18 @@ void handlerHeader_hasEnumAndCallbacksPerEvent() { assertFalse(h.contains("SetBadExceptionCallback"), "Exception should not get a callback: " + h); } + @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 initialResponseHeader_hasHeaderCollectionCtorAndSerdeDecls() { String h = render("DoStreamInitialResponse.h"); From 767d0b8028795cbff741a048f7e8be170a2e9429 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Wed, 29 Jul 2026 14:07:23 -0400 Subject: [PATCH 15/18] Add event-stream augmentation to RequestRenderer --- .../model/renderers/RequestRenderer.java | 50 ++++++++ .../generators/model/RequestRendererTest.java | 118 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/RequestRenderer.java index 888880b4da7..1089ab11615 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 @@ -9,6 +9,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; +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 software.amazon.smithy.model.Model; @@ -82,6 +83,13 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.write(""); writer.write("#include "); } + + boolean streamingResponse = ShapeClassifier.isEventStreamResponseOperation(operation, model); + boolean streamingRequest = ShapeClassifier.isEventStreamRequestOperation(operation, model); + if (streamingResponse) { + writer.write("#include "); + writer.write("#include ", smithyServiceName, operation.getId().getName()); + } writer.write(""); writer.writeNamespaceOpen("Aws"); @@ -104,10 +112,47 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.write("inline virtual const char* GetServiceRequestName() const override { return \"$L\"; }", operation.getId().getName()); writer.write(""); + if (streamingRequest) { + writer.write("inline virtual bool IsEventStreamRequest() const override { return true; }"); + } + if (streamingResponse) { + writer.write("inline virtual bool HasEventStreamResponse() const override { return true; }"); + } writer.write("$L Aws::String SerializePayload() const override;", exportMacro); writer.write(""); writer.write("$L Aws::Http::HeaderValueCollection GetRequestSpecificHeaders() const override;", exportMacro); + if (streamingResponse) { + String handlerType = operation.getId().getName() + "Handler"; + writer.write(""); + writer.write("/**"); + writer.write(" * Underlying Event Stream Decoder."); + writer.write(" */"); + writer.write("inline Aws::Utils::Event::EventStreamDecoder& GetEventStreamDecoder() { return m_decoder; }"); + writer.write(""); + writer.write("/**"); + writer.write(" * Underlying Event Stream Handler which is used to define callback functions."); + writer.write(" */"); + writer.write("inline $1L& GetEventStreamHandler() { return m_handler; }", handlerType); + writer.write(""); + writer.write("/**"); + writer.write(" * Underlying Event Stream Handler which is used to define callback functions."); + writer.write(" */"); + writer.openBlock("inline void SetEventStreamHandler(const $1L& value) {", "}", handlerType, () -> { + writer.write("m_handler = value;"); + writer.write("m_decoder.ResetEventStreamHandler(&m_handler);"); + }); + writer.write(""); + writer.write("/**"); + writer.write(" * Underlying Event Stream Handler which is used to define callback functions."); + writer.write(" */"); + writer.openBlock("inline $1LRequest& WithEventStreamHandler(const $2L& value) {", "}", + operation.getId().getName(), handlerType, () -> { + writer.write("SetEventStreamHandler(value);"); + writer.write("return *this;"); + }); + } + if (hasEndpointContextParams(operation, shape)) { writer.write(""); writer.write("/**"); @@ -123,6 +168,11 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.dedent(); writer.write("private:"); writer.indent(); + if (streamingResponse) { + String handlerType = operation.getId().getName() + "Handler"; + writer.write("$1L m_handler;", handlerType); + writer.write("Aws::Utils::Event::EventStreamDecoder m_decoder{Utils::Event::EventStreamDecoder(&m_handler)};"); + } MemberRenderer.renderPrivateSection(writer, shape, model); } }); 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 new file mode 100644 index 00000000000..fda307537e6 --- /dev/null +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/test/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/RequestRendererTest.java @@ -0,0 +1,118 @@ +/** + * 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.renderers.RequestRenderer; +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; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.StreamingTrait; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RequestRendererTest { + + private static Model streamingModel(boolean inputStreams, boolean outputStreams) { + StringShape str = StringShape.builder().id("com.example#String").build(); + StructureShape eventA = StructureShape.builder() + .id("com.example#AlphaEvent") + .addMember("data", str.getId()) + .build(); + UnionShape stream = UnionShape.builder() + .id("com.example#MyStreamEventStream") + .addTrait(new StreamingTrait()) + .addMember("alpha", eventA.getId()) + .build(); + + StructureShape.Builder inputBuilder = StructureShape.builder().id("com.example#DoStreamInput"); + inputBuilder.addMember("name", str.getId()); + if (inputStreams) { + inputBuilder.addMember("body", stream.getId()); + } + StructureShape input = inputBuilder.build(); + + StructureShape.Builder outputBuilder = StructureShape.builder().id("com.example#DoStreamOutput"); + if (outputStreams) { + outputBuilder.addMember("stream", stream.getId()); + } else { + outputBuilder.addMember("result", str.getId()); + } + StructureShape output = outputBuilder.build(); + + OperationShape op = 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, eventA, stream, input, output, op, service).build(); + } + + private static String renderRequestHeaderForStreamingOp(boolean inputStreams, boolean outputStreams) { + Model model = streamingModel(inputStreams, outputStreams); + ServiceShape service = model.expectShape(ShapeId.from("com.example#Example"), ServiceShape.class); + MockManifest manifest = new MockManifest(); + CppWriterDelegator delegator = new CppWriterDelegator(manifest); + RequestRenderer renderer = new RequestRenderer( + ShapeClassifier.classify(model, service).requests(), + model, service, ProtocolResolver.resolve(service, model), + "Example", "AWS_EXAMPLE_API", "example"); + renderer.render(delegator); + delegator.flushWriters(); + return manifest.getFileString( + manifest.getFiles().stream() + .filter(p -> p.toString().endsWith("DoStreamRequest.h")) + .findFirst().orElseThrow()) + .orElseThrow(); + } + + @Test + void streamingResponseRequest_hasEventStreamAugmentation() { + // Model: operation with streaming OUTPUT only (like SubscribeToShard / ConverseStream) + String h = renderRequestHeaderForStreamingOp(false, true); + assertTrue(h.contains("bool HasEventStreamResponse() const override { return true; }"), + "Missing HasEventStreamResponse: " + h); + assertTrue(h.contains("GetEventStreamDecoder()"), "Missing decoder accessor: " + h); + assertTrue(h.contains("GetEventStreamHandler()"), "Missing handler accessor: " + h); + assertTrue(h.contains("SetEventStreamHandler") && h.contains("WithEventStreamHandler"), + "Missing handler setters: " + h); + assertTrue(h.contains("m_decoder.ResetEventStreamHandler(&m_handler)"), + "Missing decoder reset in setter: " + h); + assertTrue(h.contains("EventStreamDecoder m_decoder"), "Missing decoder member: " + h); + assertTrue(h.contains("#include "), + "Missing decoder include: " + h); + assertFalse(h.contains("IsEventStreamRequest"), + "Response-only op must not declare IsEventStreamRequest: " + h); + } + + @Test + void bidirectionalRequest_alsoHasIsEventStreamRequest() { + String h = renderRequestHeaderForStreamingOp(true, true); + assertTrue(h.contains("bool IsEventStreamRequest() const override { return true; }"), + "Missing IsEventStreamRequest for bidirectional op: " + h); + assertTrue(h.contains("bool HasEventStreamResponse() const override { return true; }"), + "Bidirectional op must also declare HasEventStreamResponse: " + h); + } + + @Test + void nonStreamingRequest_hasNoEventStreamAugmentation() { + String h = renderRequestHeaderForStreamingOp(false, false); + assertFalse(h.contains("HasEventStreamResponse"), "Non-streaming request must be clean: " + h); + assertFalse(h.contains("GetEventStreamDecoder"), "Non-streaming request must be clean: " + h); + assertFalse(h.contains("IsEventStreamRequest"), "Non-streaming request must be clean: " + h); + } +} From b9e58f563029187897593838783e524a5d81b157 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 30 Jul 2026 13:33:45 -0400 Subject: [PATCH 16/18] Generate event stream model artifacts in Smithy codegen Add response-side event stream generation (handler, initial response, and event stream union) plus request augmentation for simplex streaming operations, closing the gap versus C2J for kinesis SubscribeToShard and bedrock-runtime streaming operations. - Add EventStreamRenderer producing the handler and initial-response (with serde stubs) and the header-only event stream union, matching C2J which emits no .cpp for the union. - Augment RequestRenderer with HasEventStreamResponse/IsEventStreamRequest and the decoder/handler accessors, placing members to match C2J ordering. - Add event stream detection helpers to ShapeClassifier and event-stream serde stub helpers to SerdeStub. - Restore handler and union documentation to match C2J. - Deduplicate renderClassDocComment into MemberRenderer and emit the file copyright header once from CppWriterDelegator. --- .../BaseCompilationTestGenerator.java | 8 +-- .../generators/BaseHeaderGenerator.java | 8 +-- .../generators/BaseTraitsGenerator.java | 8 +-- .../generators/CppWriter.java | 13 ++++ .../generators/CppWriterDelegator.java | 5 +- .../generators/model/EnumRenderer.java | 10 --- .../generators/model/MemberRenderer.java | 25 +++++++ .../generators/model/SerdeStub.java | 24 +++++++ .../model/renderers/EventStreamRenderer.java | 68 ++++++++----------- .../model/renderers/RequestRenderer.java | 35 +++------- .../model/renderers/ResultRenderer.java | 27 +------- .../model/renderers/SubObjectRenderer.java | 27 +------- .../waiters/WaiterEmptyHeaderGenerator.java | 5 -- .../model/EventStreamRendererTest.java | 35 +++++++++- .../generators/model/RequestRendererTest.java | 10 +++ 15 files changed, 156 insertions(+), 152 deletions(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseCompilationTestGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseCompilationTestGenerator.java index 210be2c5c65..007b54bf32c 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseCompilationTestGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseCompilationTestGenerator.java @@ -58,12 +58,8 @@ private void render(CppWriter writer) { } private void writeHeader(CppWriter writer, String serviceName) { - writer.write("/**") - .write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.") - .write(" * SPDX-License-Identifier: Apache-2.0.") - .write(" */") - .write("") - .write("// Header compilation test for " + serviceName + " " + getTestType().toLowerCase() + " headers") + // Copyright/SPDX header is emitted by CppWriterDelegator when the file is created. + writer.write("// Header compilation test for " + serviceName + " " + getTestType().toLowerCase() + " headers") .write("// " + getTestDescription()) .write(""); } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseHeaderGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseHeaderGenerator.java index e549174a829..8ea6eb98897 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseHeaderGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseHeaderGenerator.java @@ -39,12 +39,8 @@ public final void render(CppWriter writer) { } private void writeHeader(CppWriter writer) { - writer.write("/**") - .write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.") - .write(" * SPDX-License-Identifier: Apache-2.0.") - .write(" */") - .write("") - .write("#pragma once"); + // Copyright/SPDX header is emitted by CppWriterDelegator when the file is created. + writer.write("#pragma once"); } private void writeNamespaceOpen(CppWriter writer, String serviceName) { diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseTraitsGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseTraitsGenerator.java index 7dfc99f9ae6..09696b0c88a 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseTraitsGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/BaseTraitsGenerator.java @@ -46,11 +46,7 @@ public final void write() { } private void writeHeader(CppWriter writer) { - writer.write("/**") - .write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.") - .write(" * SPDX-License-Identifier: Apache-2.0.") - .write(" */") - .write("") - .write("#pragma once"); + // Copyright/SPDX header is emitted by CppWriterDelegator when the file is created. + writer.write("#pragma once"); } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriter.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriter.java index bf57c4ad3b8..6ddbed5c14b 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriter.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriter.java @@ -34,6 +34,19 @@ public CppWriter writeInclude(String header) { write("#include <$L>", header); return this; } + + /** + * Writes the standard Amazon copyright / SPDX license header that begins every + * generated file, followed by a blank line. + */ + public CppWriter writeCopyright() { + write("/**"); + write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); + write(" * SPDX-License-Identifier: Apache-2.0."); + write(" */"); + write(""); + return this; + } public CppWriter writeNamespaceOpen(String namespace) { openBlock("namespace $L\n{", namespace); diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java index 63b060d0bde..c162427b684 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java @@ -18,7 +18,10 @@ public CppWriterDelegator(FileManifest fileManifest) { } public void useFileWriter(String filename, Consumer writerConsumer) { - CppWriter writer = writers.computeIfAbsent(filename, k -> new CppWriter()); + // Every generated file begins with the standard copyright/SPDX header. Emit it once, + // when the writer is first created, so callers don't repeat it (and reopening a file + // for appending does not duplicate it). + CppWriter writer = writers.computeIfAbsent(filename, k -> new CppWriter().writeCopyright()); writerConsumer.accept(writer); } 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 0544261e7ed..c04466d3a7c 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 @@ -45,11 +45,6 @@ public static void renderHeader(CppWriter writer, Shape enumShape, String servic String enumName = enumShape.getId().getName(); List values = getEnumValues(enumShape); - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); writer.write("#pragma once"); writer.write("#include "); writer.write("#include ", @@ -107,11 +102,6 @@ public static void renderSource(CppWriter writer, Shape enumShape, String servic List values = getEnumValues(enumShape); List wireValues = getEnumWireValues(enumShape); - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); writer.write("#include ", projectName, enumName); writer.write("#include "); writer.write("#include "); 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 5b297e05052..3123f563cba 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 @@ -248,6 +248,31 @@ public static void writeDocComment(CppWriter writer, String doc) { writer.write(" */"); } + /** + * 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. + * + * @param writer the CppWriter to write to + * @param shape the shape whose class doc to render (structure or union) + * @param smithyServiceName the service name used in the reference URL (e.g. "kinesis") + * @param version the service API version used in the reference URL + */ + public static void renderClassDocComment(CppWriter writer, Shape shape, + String smithyServiceName, String version) { + if (shape.getTrait(DocumentationTrait.class).isPresent()) { + String docText = collapseWhitespace(shape.getTrait(DocumentationTrait.class).get().getValue()); + String seeAlso = String.format( + "

    See Also:

    AWS API Reference

    ", + smithyServiceName, version, shape.getId().getName()); + writeDocComment(writer, docText + seeAlso); + } else { + writer.write("/**"); + writer.write(" */"); + } + } + public static String collapseWhitespace(String text) { if (text == null) { return null; diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java index d383740f107..0efd09b7dc2 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/SerdeStub.java @@ -20,20 +20,44 @@ private SerdeStub() { public static void renderHeaderDeclarations(CppWriter writer, Protocol protocol, String exportMacro, String className) { + renderHeaderDeclarations(writer, protocol, exportMacro, className, null); + } + + /** + * Emits the protocol-specific serde method declarations for a class. + * + * @param beforeSerializeMethod optional hook run after the (de)serialization constructor + * and {@code operator=} declarations but before the serialize method + * ({@code Jsonize}/{@code AddToNode}/{@code OutputToStream}). Used to inject + * additional constructors (e.g. the event-stream initial-response header ctor) + * at the mainline position. Ignored when {@code null}. + */ + public static void renderHeaderDeclarations(CppWriter writer, Protocol protocol, + String exportMacro, String className, + Runnable beforeSerializeMethod) { if (protocol.isJsonLike()) { writer.write("$L $L() = default;", exportMacro, className); writer.write("$L $L(Aws::Utils::Json::JsonView jsonValue);", exportMacro, className); writer.write("$L $L& operator=(Aws::Utils::Json::JsonView jsonValue);", exportMacro, className); + if (beforeSerializeMethod != null) { + beforeSerializeMethod.run(); + } writer.write("$L Aws::Utils::Json::JsonValue Jsonize() const;", exportMacro); } else if (protocol == Protocol.REST_XML) { writer.write("$L $L() = default;", exportMacro, className); writer.write("$L $L(const Aws::Utils::Xml::XmlNode& xmlNode);", exportMacro, className); writer.write("$L $L& operator=(const Aws::Utils::Xml::XmlNode& xmlNode);", exportMacro, className); + if (beforeSerializeMethod != null) { + beforeSerializeMethod.run(); + } writer.write("$L void AddToNode(Aws::Utils::Xml::XmlNode& parentNode) const;", exportMacro); } else if (protocol == Protocol.QUERY_XML || protocol == Protocol.EC2) { writer.write("$L $L() = default;", exportMacro, className); writer.write("$L $L(const Aws::Utils::Xml::XmlNode& xmlNode);", exportMacro, className); writer.write("$L $L& operator=(const Aws::Utils::Xml::XmlNode& xmlNode);", exportMacro, className); + if (beforeSerializeMethod != null) { + beforeSerializeMethod.run(); + } writer.write("$L void OutputToStream(Aws::OStream& ostream, const char* location, unsigned index, const char* locationValue) const;", exportMacro); writer.write("$L void OutputToStream(Aws::OStream& ostream, const char* location) const;", exportMacro); } else { 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 ea7bb847559..4d30d1aeb9f 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 @@ -7,6 +7,7 @@ import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriter; import com.amazonaws.util.awsclientsmithygenerator.generators.CppWriterDelegator; import com.amazonaws.util.awsclientsmithygenerator.generators.model.CppTypeMapper; +import com.amazonaws.util.awsclientsmithygenerator.generators.model.MemberRenderer; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ProtocolResolver.Protocol; import com.amazonaws.util.awsclientsmithygenerator.generators.model.SerdeStub; import com.amazonaws.util.awsclientsmithygenerator.generators.model.ShapeClassifier.EventStreamInfo; @@ -17,6 +18,7 @@ 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; @@ -117,7 +119,6 @@ private void renderHandlerHeader(CppWriterDelegator writerDelegator, String opNa String className = opName + "Handler"; String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); writer.write("#pragma once"); writer.write("#include "); writer.write("#include "); @@ -162,12 +163,23 @@ private void renderHandlerHeader(CppWriterDelegator writerDelegator, String opNa writer.write(""); writer.write("$1L virtual void OnEvent() override;", exportMacro); writer.write(""); + writer.write("///@{"); + writer.write("/**"); + writer.write(" * Sets an initial response callback. This callback gets called on the initial $1L Operation response.", opName); + writer.write(" * This can be either \"initial-response\" decoded event frame or decoded HTTP headers received on connection."); + writer.write(" * This callback may get called more than once (i.e. on connection headers received and then on the initial-response event received)."); + writer.write(" * @param callback"); + writer.write(" */"); writer.write("inline void SetInitialResponseCallbackEx(const $1LInitialResponseCallbackEx& callback) { m_onInitialResponse = callback; }", opName); + writer.write("/**"); + writer.write(" * Sets an initial response callback (a legacy one that does not distinguish whether response originates from headers or from the event)."); + writer.write(" */"); writer.openBlock("inline void SetInitialResponseCallback(const $1LInitialResponseCallback& noArgCallback) {", "}", opName, () -> { writer.write("m_onInitialResponse = [noArgCallback](const $1LInitialResponse& rs, const Utils::Event::InitialResponseType) {", opName); writer.write("return noArgCallback(rs);"); writer.write("};"); }); + writer.write("///@}"); for (MemberShape event : events) { String ev = eventShapeName(event); writer.write("inline void Set$1LCallback(const $1LCallback& callback) { m_on$1L = callback; }", ev); @@ -210,7 +222,6 @@ private void renderHandlerSource(CppWriterDelegator writerDelegator, String opNa String fileName = "source/model/" + className + ".cpp"; String tag = opName.toUpperCase() + "_HANDLER_CLASS_TAG"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); writer.write("#include "); writer.write("#include "); writer.write("#include "); @@ -402,7 +413,6 @@ private void renderInitialResponse(CppWriterDelegator writerDelegator, String op String headerFile = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; writerDelegator.useFileWriter(headerFile, writer -> { - writeCopyright(writer); writer.write("#pragma once"); writer.write("#include "); writer.write("#include ", smithyServiceName, namespace); @@ -414,8 +424,9 @@ private void renderInitialResponse(CppWriterDelegator writerDelegator, String op writer.write(""); writer.openBlock("class $L {", "};", className, () -> { writer.write("public:"); - SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className); - writer.write("$1L $2L(const Http::HeaderValueCollection& responseHeaders);", exportMacro, className); + // The header-collection ctor sits before the serialize method (mainline ordering). + SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className, + () -> writer.write("$1L $2L(const Http::HeaderValueCollection& responseHeaders);", exportMacro, className)); }); writer.write(""); writer.writeNamespaceClose("Model"); @@ -425,7 +436,6 @@ private void renderInitialResponse(CppWriterDelegator writerDelegator, String op String sourceFile = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(sourceFile, writer -> { - writeCopyright(writer); if (protocol.isJsonLike()) { writer.write("#include "); } else { @@ -476,7 +486,6 @@ private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String o String headerFile = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; writerDelegator.useFileWriter(headerFile, writer -> { - writeCopyright(writer); writer.write("#pragma once"); writer.write("#include ", smithyServiceName, namespace); writer.write("#include ", smithyServiceName, namespace); @@ -491,6 +500,7 @@ private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String o writer.writeNamespaceOpen(namespace); writer.writeNamespaceOpen("Model"); writer.write(""); + MemberRenderer.renderClassDocComment(writer, union, smithyServiceName, service.getVersion()); writer.openBlock("class $L {", "};", className, () -> { writer.write("public:"); SerdeStub.renderHeaderDeclarations(writer, protocol, exportMacro, className); @@ -498,11 +508,11 @@ private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String o // Event member accessors, typed as their concrete shape. for (MemberShape event : events) { String cppType = CppTypeMapper.getCppType(model.expectShape(event.getTarget()), model); - renderShapeAccessor(writer, className, cppType, event.getMemberName()); + renderShapeAccessor(writer, className, cppType, event.getMemberName(), event); } // Exception member accessors, typed as Error. for (MemberShape exc : exceptions) { - renderShapeAccessor(writer, className, errorType, exc.getMemberName()); + renderShapeAccessor(writer, className, errorType, exc.getMemberName(), exc); } writer.dedent(); writer.write("private:"); @@ -529,26 +539,9 @@ private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String o writer.writeNamespaceClose("Aws"); }); - String sourceFile = "source/model/" + className + ".cpp"; - writerDelegator.useFileWriter(sourceFile, writer -> { - writeCopyright(writer); - if (protocol.isJsonLike()) { - writer.write("#include "); - } else { - writer.write("#include "); - } - writer.write("#include ", smithyServiceName, className); - writer.write(""); - writer.write("using namespace Aws::$1L::Model;", namespace); - if (protocol.isJsonLike()) { - writer.write("using namespace Aws::Utils::Json;"); - } else { - writer.write("using namespace Aws::Utils::Xml;"); - } - writer.write("using namespace Aws::Utils;"); - writer.write(""); - SerdeStub.renderSerdeSourceStub(writer, protocol, className); - }); + // 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. } /** @@ -557,12 +550,18 @@ private void renderEventStreamUnion(CppWriterDelegator writerDelegator, String o * 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) { + private void renderShapeAccessor(CppWriter writer, String className, String cppType, String memberName, + MemberShape member) { String getter = capitalize(memberName); String field = "m_" + decapitalize(memberName); String templateParam = getter + "T"; writer.write("///@{"); - 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); @@ -586,11 +585,4 @@ private static String decapitalize(String s) { return s.substring(0, 1).toLowerCase() + s.substring(1); } - private void writeCopyright(CppWriter writer) { - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); - } } 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 1089ab11615..44e02264a0c 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 @@ -17,7 +17,6 @@ import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.rulesengine.traits.ContextParamTrait; import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait; @@ -64,7 +63,6 @@ private void renderHeader(CppWriterDelegator writerDelegator, String className = operation.getId().getName() + "Request"; String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); writer.write("#pragma once"); Set includes = new TreeSet<>(); @@ -97,7 +95,7 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.writeNamespaceOpen("Model"); writer.write(""); - renderClassDocComment(writer, shape); + MemberRenderer.renderClassDocComment(writer, shape, smithyServiceName, service.getVersion()); String baseClass = namespace + "Request"; writer.openBlock("class $L : public $L {", "};", className, baseClass, () -> { @@ -169,11 +167,17 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.write("private:"); writer.indent(); if (streamingResponse) { + // Mainline places the handler/decoder after the data members and + // before the HasBeenSet flags. + MemberRenderer.renderPrivateDataMembers(writer, shape, model); String handlerType = operation.getId().getName() + "Handler"; writer.write("$1L m_handler;", handlerType); writer.write("Aws::Utils::Event::EventStreamDecoder m_decoder{Utils::Event::EventStreamDecoder(&m_handler)};"); + writer.write(""); + MemberRenderer.renderPrivateHasBeenSetFlags(writer, shape, model); + } else { + MemberRenderer.renderPrivateSection(writer, shape, model); } - MemberRenderer.renderPrivateSection(writer, shape, model); } }); writer.write(""); @@ -189,7 +193,6 @@ private void renderSource(CppWriterDelegator writerDelegator, String className = operation.getId().getName() + "Request"; String fileName = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); if (protocol.isJsonLike()) { writer.write("#include "); @@ -289,26 +292,4 @@ public Void booleanNode(software.amazon.smithy.model.node.BooleanNode node) { }); } - private void renderClassDocComment(CppWriter writer, StructureShape shape) { - if (shape.getTrait(DocumentationTrait.class).isPresent()) { - String docText = MemberRenderer.collapseWhitespace( - shape.getTrait(DocumentationTrait.class).get().getValue()); - String version = service.getVersion(); - String seeAlso = String.format( - "

    See Also:

    AWS API Reference

    ", - smithyServiceName, version, shape.getId().getName()); - MemberRenderer.writeDocComment(writer, docText + seeAlso); - } else { - writer.write("/**"); - writer.write(" */"); - } - } - - private void writeCopyright(CppWriter writer) { - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); - } } 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 4ffa1c62d89..f1741bcb084 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 @@ -59,7 +59,6 @@ private void renderHeader(CppWriterDelegator writerDelegator, String className = operation.getId().getName() + "Result"; String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); writer.write("#pragma once"); Set includes = new TreeSet<>(); @@ -86,7 +85,7 @@ private void renderHeader(CppWriterDelegator writerDelegator, writer.writeNamespaceOpen("Model"); if (shape.getTrait(DocumentationTrait.class).isPresent()) { - renderClassDocComment(writer, shape); + MemberRenderer.renderClassDocComment(writer, shape, smithyServiceName, service.getVersion()); } writer.openBlock("class $L {", "};", className, () -> { @@ -145,7 +144,6 @@ private void renderSource(CppWriterDelegator writerDelegator, String className = operation.getId().getName() + "Result"; String fileName = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); writer.write("#include "); writer.write("#include "); @@ -205,27 +203,4 @@ private void renderForwardDeclarations(CppWriter writer) { writer.writeNamespaceClose("Utils"); } } - - private void renderClassDocComment(CppWriter writer, StructureShape shape) { - if (shape.getTrait(DocumentationTrait.class).isPresent()) { - String docText = MemberRenderer.collapseWhitespace( - shape.getTrait(DocumentationTrait.class).get().getValue()); - String version = service.getVersion(); - String seeAlso = String.format( - "

    See Also:

    AWS API Reference

    ", - smithyServiceName, version, shape.getId().getName()); - MemberRenderer.writeDocComment(writer, docText + seeAlso); - } else { - writer.write("/**"); - writer.write(" */"); - } - } - - private void writeCopyright(CppWriter writer) { - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); - } } 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 5038e949c16..5c4d3fab022 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 @@ -15,7 +15,6 @@ import software.amazon.smithy.model.shapes.ServiceShape; 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; import java.util.Set; @@ -63,7 +62,6 @@ private void renderHeader(CppWriterDelegator writerDelegator, StructureShape sha String className = shape.getId().getName(); String fileName = "include/aws/" + smithyServiceName + "/model/" + className + ".h"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); writer.write("#pragma once"); // Includes @@ -90,7 +88,7 @@ private void renderHeader(CppWriterDelegator writerDelegator, StructureShape sha writer.writeNamespaceOpen("Model"); writer.write(""); - renderClassDocComment(writer, shape); + MemberRenderer.renderClassDocComment(writer, shape, smithyServiceName, service.getVersion()); writer.openBlock("class $L {", "};", className, () -> { writer.write("public:"); @@ -114,7 +112,6 @@ private void renderSource(CppWriterDelegator writerDelegator, StructureShape sha String className = shape.getId().getName(); String fileName = "source/model/" + className + ".cpp"; writerDelegator.useFileWriter(fileName, writer -> { - writeCopyright(writer); if (protocol.isJsonLike()) { writer.write("#include "); @@ -167,26 +164,4 @@ private void renderForwardDeclarations(CppWriter writer) { } } - private void renderClassDocComment(CppWriter writer, StructureShape shape) { - if (shape.getTrait(DocumentationTrait.class).isPresent()) { - String docText = MemberRenderer.collapseWhitespace( - shape.getTrait(DocumentationTrait.class).get().getValue()); - String version = service.getVersion(); - String seeAlso = String.format( - "

    See Also:

    AWS API Reference

    ", - smithyServiceName, version, shape.getId().getName()); - MemberRenderer.writeDocComment(writer, docText + seeAlso); - } else { - writer.write("/**"); - writer.write(" */"); - } - } - - private void writeCopyright(CppWriter writer) { - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); - } } diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/waiters/WaiterEmptyHeaderGenerator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/waiters/WaiterEmptyHeaderGenerator.java index af15a7d96fb..1b2cc978dc7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/waiters/WaiterEmptyHeaderGenerator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/waiters/WaiterEmptyHeaderGenerator.java @@ -36,11 +36,6 @@ public static void generate(PluginContext context) { CppWriterDelegator writers = new CppWriterDelegator(context.getFileManifest()); writers.useFileWriter("include/aws/" + c2jName + "/" + serviceName + "Waiter.h", writer -> { - writer.write("/**"); - writer.write(" * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."); - writer.write(" * SPDX-License-Identifier: Apache-2.0."); - writer.write(" */"); - writer.write(""); writer.write("#pragma once"); writer.write(""); writer.writeNamespaceOpen("Aws"); 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 225bb0b6ab9..78d86e3d000 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 @@ -14,6 +14,7 @@ import software.amazon.smithy.model.shapes.StringShape; 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; @@ -40,7 +41,9 @@ private static Model twoEventModel() { UnionShape stream = UnionShape.builder() .id("com.example#MyStreamEventStream") .addTrait(new StreamingTrait()) - .addMember("alpha", eventA.getId()) + .addTrait(new DocumentationTrait("

    Tagged union of stream events.

    ")) + .addMember("alpha", eventA.getId(), + b -> b.addTrait(new DocumentationTrait("

    Alpha event doc.

    "))) .addMember("beta", eventB.getId()) .addMember("badException", exc.getId()) .build(); @@ -100,6 +103,21 @@ void handlerHeader_hasEnumAndCallbacksPerEvent() { assertFalse(h.contains("SetBadExceptionCallback"), "Exception should not get a callback: " + h); } + @Test + void handlerHeader_wrapsInitialResponseSettersInDocGroup() { + String h = render("DoStreamHandler.h"); + assertTrue(h.contains("Sets an initial response callback."), + "Missing initial-response setter doc: " + h); + assertTrue(h.contains("a legacy one that does not distinguish"), + "Missing legacy initial-response setter doc: " + h); + // The two initial-response setters must be inside a ///@{ ... ///@} group. + int groupOpen = h.indexOf("///@{"); + int exSetter = h.indexOf("SetInitialResponseCallbackEx"); + int groupClose = h.indexOf("///@}", exSetter); + assertTrue(groupOpen >= 0 && groupOpen < exSetter && groupClose > exSetter, + "Initial-response setters must be wrapped in a ///@{ ... ///@} group: " + h); + } + @Test void eventStreamUnionHeader_typesEventAndExceptionMembers() { String h = render("MyStreamEventStream.h"); @@ -112,6 +130,16 @@ void eventStreamUnionHeader_typesEventAndExceptionMembers() { assertTrue(h.contains("ExampleError"), "Exception members must be typed as ExampleError: " + 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); + } + @Test void initialResponseHeader_hasHeaderCollectionCtorAndSerdeDecls() { String h = render("DoStreamInitialResponse.h"); @@ -121,6 +149,11 @@ void initialResponseHeader_hasHeaderCollectionCtorAndSerdeDecls() { // JSON serde declaration present (test model resolves to a JSON-like protocol) assertTrue(h.contains("Jsonize") || h.contains("XmlNode") || h.contains("OutputToStream"), "Missing serde declarations: " + h); + // The HeaderValueCollection ctor must precede Jsonize (mainline ordering). + int headerCtor = h.indexOf("const Http::HeaderValueCollection& responseHeaders"); + int jsonize = h.indexOf("Jsonize"); + assertTrue(headerCtor >= 0 && jsonize >= 0 && headerCtor < jsonize, + "HeaderValueCollection ctor must come before Jsonize: " + h); } @Test 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 fda307537e6..d08fe24fe09 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 @@ -97,6 +97,16 @@ 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. + int dataMember = h.indexOf("Aws::String m_name;"); + int handlerDecl = h.indexOf("DoStreamHandler m_handler;"); + int firstFlag = h.indexOf("HasBeenSet = false;"); + assertTrue(dataMember >= 0 && handlerDecl > dataMember, + "m_handler declaration must come after data members: " + h); + assertTrue(firstFlag >= 0 && handlerDecl < firstFlag, + "m_handler declaration must come before HasBeenSet flags: " + h); } @Test From 4f2daea0ae7f3c12e6728b37b242f1f41e168ea5 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 30 Jul 2026 13:52:30 -0400 Subject: [PATCH 17/18] Fix utf-8 bom --- .../generators/CppWriterDelegator.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java index c162427b684..0aef2a5a273 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/CppWriterDelegator.java @@ -10,6 +10,9 @@ import java.util.function.Consumer; public class CppWriterDelegator { + /** UTF-8 byte order mark (U+FEFF), prepended to every file to match C2J-generated output. */ + private static final String UTF8_BOM = "\uFEFF"; + private final FileManifest fileManifest; private final Map writers = new HashMap<>(); @@ -28,7 +31,7 @@ public void useFileWriter(String filename, Consumer writerConsumer) { public void flushWriters() { writers.forEach((filename, writer) -> { // Add UTF-8 BOM to match C2J-generated file format - String content = "" + writer.toString(); + String content = UTF8_BOM + writer.toString(); fileManifest.writeFile(filename, content); }); } From 29d03d1ed4c092e6032a1af77688d61fc2efcf53 Mon Sep 17 00:00:00 2001 From: sbaluja Date: Thu, 30 Jul 2026 14:15:09 -0400 Subject: [PATCH 18/18] Null checks -> Optional --- .../generators/model/MemberRenderer.java | 9 ++++++--- .../model/renderers/EventStreamRenderer.java | 14 ++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) 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 3123f563cba..e9554b96b1c 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 @@ -273,10 +273,13 @@ public static void renderClassDocComment(CppWriter writer, Shape shape, } } + /** + * Collapses all runs of whitespace in {@code text} to single spaces and trims the result. + * + * @param text the documentation text to normalize; must not be null + * @return the whitespace-collapsed text + */ public static String collapseWhitespace(String text) { - if (text == null) { - return null; - } return text.replaceAll("\\s+", " ").trim(); } 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 4d30d1aeb9f..52567d284e7 100644 --- a/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java +++ b/tools/code-generation/smithy/cpp-codegen/smithy-cpp-codegen/src/main/java/com/amazonaws/util/awsclientsmithygenerator/generators/model/renderers/EventStreamRenderer.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * Renders C++ event stream artifacts for response-side (simplex) streaming operations: @@ -59,10 +60,11 @@ public EventStreamRenderer(List eventStreams, Model model, Serv @Override public void render(CppWriterDelegator writerDelegator) { for (EventStreamInfo info : eventStreams) { - UnionShape union = findStreamingUnion(info.resultShape()); - if (union == null) { + Optional streamingUnion = findStreamingUnion(info.resultShape()); + if (streamingUnion.isEmpty()) { continue; } + UnionShape union = streamingUnion.get(); List events = new ArrayList<>(); List exceptions = new ArrayList<>(); partitionMembers(union, events, exceptions); @@ -74,15 +76,15 @@ public void render(CppWriterDelegator writerDelegator) { } } - /** Finds the @streaming union targeted by a member of the result structure. */ - private UnionShape findStreamingUnion(StructureShape resultShape) { + /** Finds the @streaming union targeted by a member of the result structure, if any. */ + private Optional findStreamingUnion(StructureShape resultShape) { for (MemberShape member : resultShape.getAllMembers().values()) { Shape target = model.expectShape(member.getTarget()); if (target.isUnionShape() && target.hasTrait(StreamingTrait.class)) { - return target.asUnionShape().get(); + return target.asUnionShape(); } } - return null; + return Optional.empty(); } /** Splits union members into events (non-exception) and exceptions, preserving order. */