diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/Schema.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/Schema.h index a8faa19266d..035d56d20cd 100644 --- a/src/aws-cpp-sdk-core/include/smithy/client/schema/Schema.h +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/Schema.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -48,6 +49,15 @@ class Schema { uint16_t GetMemberCount() const { return m_memberCount; } + // XML traits accessor + const XmlTraits* GetXmlTraits() const { return m_xmlTraits; } + + // Builder-style setter for XML traits pointer (used by codegen) + Schema& WithXmlTraits(const XmlTraits* traits) { + m_xmlTraits = traits; + return *this; + } + private: const char* m_id = nullptr; ShapeType m_type = ShapeType::Structure; @@ -55,6 +65,7 @@ class Schema { int m_memberIndex = 0; const Schema* m_members = nullptr; uint16_t m_memberCount = 0; + const XmlTraits* m_xmlTraits = nullptr; }; } // namespace schema diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlShapeSerializer.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlShapeSerializer.h index 4c894024cf7..bbf730bf061 100644 --- a/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlShapeSerializer.h +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlShapeSerializer.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -9,6 +10,7 @@ namespace schema { class SMITHY_API XmlShapeSerializer final : public ShapeSerializer { public: + using SerializerOutcome = Aws::Utils::Outcome>; XmlShapeSerializer(); ~XmlShapeSerializer(); @@ -25,6 +27,8 @@ class SMITHY_API XmlShapeSerializer final : public ShapeSerializer { void WriteEnum(const Schema& schema, int value) override; void WriteNull(const Schema& schema) override; + void WriteAttribute(const Schema& schema, const Aws::String& value); + bool BeginList(const Schema& schema, size_t count) override; void EndList() override; @@ -35,7 +39,7 @@ class SMITHY_API XmlShapeSerializer final : public ShapeSerializer { bool BeginNestedStructure(const Schema& schema) override; void EndNestedStructure() override; - Aws::String GetPayload() const; + SerializerOutcome GetPayload(); private: class Impl; diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlTraits.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlTraits.h new file mode 100644 index 00000000000..46671406f36 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlTraits.h @@ -0,0 +1,30 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#pragma once + +#include + +namespace smithy { +namespace schema { + +/** + * XML-specific traits for a Schema member. + * Instances should be declared as static const and referenced via pointer from Schema. + * Zero cost for non-XML services (Schema just holds a null pointer). + */ +struct XmlTraits { + Aws::String xmlName; // @xmlName override (empty = use member name) + Aws::String listItemName; // element name for list items (default: "member") + Aws::String mapEntryName; // element name for map entries (default: "entry") + Aws::String mapKeyName; // element name for map keys (default: "key") + Aws::String mapValueName; // element name for map values (default: "value") + Aws::String namespaceUri; // @xmlNamespace URI + Aws::String namespacePrefix; // @xmlNamespace prefix (empty = default namespace) + bool flattened = false; // @xmlFlattened + bool isAttribute = false; // @xmlAttribute +}; + +} // namespace schema +} // namespace smithy diff --git a/src/aws-cpp-sdk-core/source/smithy/client/schema/XmlShapeSerializer.cpp b/src/aws-cpp-sdk-core/source/smithy/client/schema/XmlShapeSerializer.cpp new file mode 100644 index 00000000000..22a204ea6a2 --- /dev/null +++ b/src/aws-cpp-sdk-core/source/smithy/client/schema/XmlShapeSerializer.cpp @@ -0,0 +1,417 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#include +#include +#include + +using namespace smithy::schema; +using namespace Aws::Utils; + +static constexpr int MAX_DEPTH = 1000; + +namespace { + +void WriteXmlEscaped(Aws::String& buf, const Aws::String& value) { + const char* data = value.data(); + const size_t len = value.size(); + size_t i = 0; + while (i < len) { + size_t start = i; + while (i < len && data[i] != '&' && data[i] != '<' && data[i] != '>' && data[i] != '"' && data[i] != '\'') { + i++; + } + if (i > start) { + buf.append(data + start, i - start); + } + if (i < len) { + switch (data[i]) { + case '&': + buf += "&"; + break; + case '<': + buf += "<"; + break; + case '>': + buf += ">"; + break; + case '"': + buf += """; + break; + case '\'': + buf += "'"; + break; + } + i++; + } + } +} + +// Get the effective XML element name for a schema. +// If XmlTraits is present and has an xmlName, use it; otherwise fall back to memberName. +Aws::String GetXmlName(const Schema& schema) { + const auto* traits = schema.GetXmlTraits(); + if (traits && !traits->xmlName.empty()) { + return traits->xmlName; + } + return schema.GetMemberName(); +} + +bool IsFlattened(const Schema& schema) { + const auto* traits = schema.GetXmlTraits(); + return traits && traits->flattened; +} + +Aws::String GetListItemName(const Schema& schema) { + const auto* traits = schema.GetXmlTraits(); + if (traits && !traits->listItemName.empty()) { + return traits->listItemName; + } + return "member"; +} + +Aws::String GetMapEntryName(const Schema& schema) { + const auto* traits = schema.GetXmlTraits(); + if (traits && !traits->mapEntryName.empty()) { + return traits->mapEntryName; + } + return "entry"; +} + +Aws::String GetMapKeyName(const Schema& schema) { + const auto* traits = schema.GetXmlTraits(); + if (traits && !traits->mapKeyName.empty()) { + return traits->mapKeyName; + } + return "key"; +} + +Aws::String GetMapValueName(const Schema& schema) { + const auto* traits = schema.GetXmlTraits(); + if (traits && !traits->mapValueName.empty()) { + return traits->mapValueName; + } + return "value"; +} + +} // anonymous namespace + +class XmlShapeSerializer::Impl { + public: + Impl() { m_buf.reserve(8192); } + + bool BeginStructure(const Schema& schema) { + if (m_depth + 1 >= MAX_DEPTH) { + m_errorMessage = "Maximum serialization depth exceeded"; + return false; + } + FlushOpenTag(); + const auto name = GetXmlName(schema); + if (!name.empty()) { + m_buf += '<'; + m_buf += name; + const auto* traits = schema.GetXmlTraits(); + if (traits && !traits->namespaceUri.empty()) { + if (traits->namespacePrefix.empty()) { + m_buf += " xmlns=\""; + } else { + m_buf += " xmlns:"; + m_buf += traits->namespacePrefix; + m_buf += "=\""; + } + m_buf += traits->namespaceUri; + m_buf += '"'; + } + m_hasOpenTag = true; + } + m_depth++; + m_tagStack[m_depth] = name; + return true; + } + + void EndStructure() { + FlushOpenTag(); + const auto& name = m_tagStack[m_depth]; + m_depth--; + if (!name.empty()) { + CloseTag(name); + } + } + + void WriteAttribute(const Schema& schema, const Aws::String& value) { + if (!m_hasOpenTag) return; + const auto name = GetXmlName(schema); + m_buf += ' '; + m_buf += name; + m_buf += "=\""; + WriteXmlEscaped(m_buf, value); + m_buf += '"'; + } + + void WriteBoolean(const Schema& schema, bool value) { WrapValue(GetXmlName(schema), value ? "true" : "false"); } + + void WriteInteger(const Schema& schema, int value) { WrapValue(GetXmlName(schema), StringUtils::to_string(value)); } + + void WriteLong(const Schema& schema, int64_t value) { WrapValue(GetXmlName(schema), StringUtils::to_string(value)); } + + void WriteDouble(const Schema& schema, double value) { WrapValue(GetXmlName(schema), StringUtils::to_string(value)); } + + void WriteString(const Schema& schema, const Aws::String& value) { WrapEscapedValue(GetXmlName(schema), value); } + + void WriteTimestamp(const Schema& schema, const DateTime& value) { + WrapValue(GetXmlName(schema), value.ToGmtString(Aws::Utils::DateFormat::ISO_8601)); + } + + void WriteBlob(const Schema& schema, const ByteBuffer& value) { WrapValue(GetXmlName(schema), HashingUtils::Base64Encode(value)); } + + void WriteEnum(const Schema& schema, int value) { WriteInteger(schema, value); } + + void WriteNull(const Schema&) { + // XML omits null values — no output + } + + bool BeginList(const Schema& schema, size_t) { + if (m_depth + 1 >= MAX_DEPTH) { + m_errorMessage = "Maximum serialization depth exceeded"; + return false; + } + FlushOpenTag(); + const auto name = GetXmlName(schema); + if (IsFlattened(schema)) { + // Flattened: no wrapper element, items repeat with the list's xmlName + m_depth++; + m_tagStack[m_depth] = name; + m_listItemName[m_depth] = name; + m_isList[m_depth] = true; + m_isFlattened[m_depth] = true; + m_isMap[m_depth] = false; + } else { + // Non-flattened: wrap in container element, items use configurable name + if (!name.empty()) { + OpenTag(name); + } + m_depth++; + m_tagStack[m_depth] = name; + m_listItemName[m_depth] = GetListItemName(schema); + m_isList[m_depth] = true; + m_isFlattened[m_depth] = false; + m_isMap[m_depth] = false; + } + return true; + } + + void EndList() { + const auto& name = m_tagStack[m_depth]; + bool flattened = m_isFlattened[m_depth]; + m_isList[m_depth] = false; + m_depth--; + if (!flattened && !name.empty()) { + CloseTag(name); + } + } + + bool BeginMap(const Schema& schema, size_t) { + if (m_depth + 1 >= MAX_DEPTH) { + m_errorMessage = "Maximum serialization depth exceeded"; + return false; + } + FlushOpenTag(); + const auto name = GetXmlName(schema); + bool flattened = IsFlattened(schema); + if (!flattened && !name.empty()) { + OpenTag(name); + } + m_depth++; + m_tagStack[m_depth] = name; + m_isMap[m_depth] = true; + m_isList[m_depth] = false; + m_isFlattened[m_depth] = flattened; + m_mapInEntry[m_depth] = false; + m_mapEntryName[m_depth] = flattened ? name : GetMapEntryName(schema); + m_mapKeyName[m_depth] = GetMapKeyName(schema); + m_mapValueName[m_depth] = GetMapValueName(schema); + return true; + } + + void WriteMapKey(const Aws::String& key) { + OpenTag(m_mapEntryName[m_depth]); + OpenTag(m_mapKeyName[m_depth]); + WriteXmlEscaped(m_buf, key); + CloseTag(m_mapKeyName[m_depth]); + m_mapInEntry[m_depth] = true; + } + + void EndMap() { + const auto& name = m_tagStack[m_depth]; + bool flattened = m_isFlattened[m_depth]; + m_isMap[m_depth] = false; + m_depth--; + if (!flattened && !name.empty()) { + CloseTag(name); + } + } + + bool BeginNestedStructure(const Schema& schema) { + if (m_depth + 1 >= MAX_DEPTH) { + m_errorMessage = "Maximum serialization depth exceeded"; + return false; + } + FlushOpenTag(); + if (m_depth > 0 && m_isMap[m_depth] && m_mapInEntry[m_depth]) { + // Inside a map entry: open as the struct wrapper + OpenTag(m_mapValueName[m_depth]); + m_mapInEntry[m_depth] = false; + m_depth++; + m_tagStack[m_depth] = ""; + m_isList[m_depth] = false; + m_isMap[m_depth] = false; + m_isFlattened[m_depth] = false; + m_mapInEntry[m_depth] = false; + m_isMapValueStruct[m_depth] = true; + return true; + } + const auto name = GetEffectiveTag(schema); + OpenTag(name); + m_depth++; + m_tagStack[m_depth] = name; + m_isList[m_depth] = false; + m_isMap[m_depth] = false; + m_isFlattened[m_depth] = false; + m_mapInEntry[m_depth] = false; + m_isMapValueStruct[m_depth] = false; + return true; + } + + void EndNestedStructure() { + const auto& name = m_tagStack[m_depth]; + bool closeMapEntry = m_isMapValueStruct[m_depth]; + m_depth--; + if (!name.empty()) { + CloseTag(name); + } + if (closeMapEntry) { + CloseTag(m_mapValueName[m_depth]); + CloseTag(m_mapEntryName[m_depth]); + } + } + + using SerializerOutcome = XmlShapeSerializer::SerializerOutcome; + + SerializerOutcome GetPayload() { + if (m_finalized || !m_errorMessage.empty()) { + return Aws::Client::AWSError( + Aws::Client::CoreErrors::INTERNAL_FAILURE, "SerializationException", + !m_errorMessage.empty() ? m_errorMessage : "Serializer has already been finalized", false); + } + m_finalized = true; + return std::move(m_buf); + } + + private: + Aws::String m_buf; + int m_depth = 0; + bool m_finalized = false; + bool m_hasOpenTag = false; + Aws::String m_errorMessage; + Aws::Array m_tagStack{}; + Aws::Array m_listItemName{}; + Aws::Array m_isList{}; + Aws::Array m_isFlattened{}; + Aws::Array m_isMap{}; + Aws::Array m_mapInEntry{}; + Aws::Array m_isMapValueStruct{}; + Aws::Array m_mapEntryName{}; + Aws::Array m_mapKeyName{}; + Aws::Array m_mapValueName{}; + + void FlushOpenTag() { + if (m_hasOpenTag) { + m_buf += '>'; + m_hasOpenTag = false; + } + } + + void OpenTag(const Aws::String& name) { + FlushOpenTag(); + m_buf += '<'; + m_buf += name; + m_buf += '>'; + } + + void CloseTag(const Aws::String& name) { + m_buf += "'; + } + + void WrapValue(const Aws::String& tag, const Aws::String& value) { + if (m_depth > 0 && m_isMap[m_depth] && m_mapInEntry[m_depth]) { + OpenTag(m_mapValueName[m_depth]); + m_buf += value; + CloseTag(m_mapValueName[m_depth]); + CloseTag(m_mapEntryName[m_depth]); + m_mapInEntry[m_depth] = false; + } else if (m_depth > 0 && m_isList[m_depth]) { + const auto& itemName = m_listItemName[m_depth]; + OpenTag(itemName); + m_buf += value; + CloseTag(itemName); + } else { + OpenTag(tag); + m_buf += value; + CloseTag(tag); + } + } + + void WrapEscapedValue(const Aws::String& tag, const Aws::String& value) { + if (m_depth > 0 && m_isMap[m_depth] && m_mapInEntry[m_depth]) { + OpenTag(m_mapValueName[m_depth]); + WriteXmlEscaped(m_buf, value); + CloseTag(m_mapValueName[m_depth]); + CloseTag(m_mapEntryName[m_depth]); + m_mapInEntry[m_depth] = false; + } else if (m_depth > 0 && m_isList[m_depth]) { + const auto& itemName = m_listItemName[m_depth]; + OpenTag(itemName); + WriteXmlEscaped(m_buf, value); + CloseTag(itemName); + } else { + OpenTag(tag); + WriteXmlEscaped(m_buf, value); + CloseTag(tag); + } + } + + Aws::String GetEffectiveTag(const Schema& schema) const { + if (m_depth > 0 && m_isList[m_depth]) { + return m_listItemName[m_depth]; + } + return GetXmlName(schema); + } +}; + +XmlShapeSerializer::XmlShapeSerializer() : m_impl(Aws::MakeUnique("XmlShapeSerializer")) {} +XmlShapeSerializer::~XmlShapeSerializer() = default; + +bool XmlShapeSerializer::BeginStructure(const Schema& schema) { return m_impl->BeginStructure(schema); } +void XmlShapeSerializer::EndStructure() { m_impl->EndStructure(); } +void XmlShapeSerializer::WriteBoolean(const Schema& schema, bool value) { m_impl->WriteBoolean(schema, value); } +void XmlShapeSerializer::WriteInteger(const Schema& schema, int value) { m_impl->WriteInteger(schema, value); } +void XmlShapeSerializer::WriteLong(const Schema& schema, int64_t value) { m_impl->WriteLong(schema, value); } +void XmlShapeSerializer::WriteDouble(const Schema& schema, double value) { m_impl->WriteDouble(schema, value); } +void XmlShapeSerializer::WriteString(const Schema& schema, const Aws::String& value) { m_impl->WriteString(schema, value); } +void XmlShapeSerializer::WriteTimestamp(const Schema& schema, const DateTime& value) { m_impl->WriteTimestamp(schema, value); } +void XmlShapeSerializer::WriteBlob(const Schema& schema, const ByteBuffer& value) { m_impl->WriteBlob(schema, value); } +void XmlShapeSerializer::WriteEnum(const Schema& schema, int value) { m_impl->WriteEnum(schema, value); } +void XmlShapeSerializer::WriteNull(const Schema& schema) { m_impl->WriteNull(schema); } +bool XmlShapeSerializer::BeginList(const Schema& schema, size_t count) { return m_impl->BeginList(schema, count); } +void XmlShapeSerializer::EndList() { m_impl->EndList(); } +bool XmlShapeSerializer::BeginMap(const Schema& schema, size_t count) { return m_impl->BeginMap(schema, count); } +void XmlShapeSerializer::WriteMapKey(const Aws::String& key) { m_impl->WriteMapKey(key); } +void XmlShapeSerializer::EndMap() { m_impl->EndMap(); } +bool XmlShapeSerializer::BeginNestedStructure(const Schema& schema) { return m_impl->BeginNestedStructure(schema); } +void XmlShapeSerializer::EndNestedStructure() { m_impl->EndNestedStructure(); } +XmlShapeSerializer::SerializerOutcome XmlShapeSerializer::GetPayload() { return m_impl->GetPayload(); } +void XmlShapeSerializer::WriteAttribute(const Schema& schema, const Aws::String& value) { m_impl->WriteAttribute(schema, value); } diff --git a/tests/aws-cpp-sdk-core-tests/smithy/client/schema/XmlShapeSerializerTest.cpp b/tests/aws-cpp-sdk-core-tests/smithy/client/schema/XmlShapeSerializerTest.cpp new file mode 100644 index 00000000000..1cb5af538b8 --- /dev/null +++ b/tests/aws-cpp-sdk-core-tests/smithy/client/schema/XmlShapeSerializerTest.cpp @@ -0,0 +1,500 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#include +#include + +using namespace smithy::schema; + +class XmlShapeSerializerTest : public Aws::Testing::AwsCppSdkGTestSuite {}; + +// --- Scalars --- + +TEST_F(XmlShapeSerializerTest, EmptyStructure) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + s.BeginStructure(root); + s.EndStructure(); + EXPECT_EQ(s.GetPayload(), ""); +} + +TEST_F(XmlShapeSerializerTest, BooleanTrue) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("enabled", ShapeType::Boolean); + s.BeginStructure(root); + s.WriteBoolean(member, true); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("true"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, BooleanFalse) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("enabled", ShapeType::Boolean); + s.BeginStructure(root); + s.WriteBoolean(member, false); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("false"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, Integer) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("count", ShapeType::Integer); + s.BeginStructure(root); + s.WriteInteger(member, 42); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("42"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, Long) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("bigNum", ShapeType::Long); + s.BeginStructure(root); + s.WriteLong(member, 9876543210LL); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("9876543210"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, Double) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("ratio", ShapeType::Double); + s.BeginStructure(root); + s.WriteDouble(member, 3.14); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("3.14"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, String) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("name", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, "hello"); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("hello"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, EmptyString) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("name", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, ""); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find(""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, Timestamp) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("created", ShapeType::Timestamp); + s.BeginStructure(root); + Aws::Utils::DateTime dt(1234567890.0); + s.WriteTimestamp(member, dt); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("2009-02-13T23:31:30Z"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, Blob) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("data", ShapeType::Blob); + s.BeginStructure(root); + unsigned char raw[] = {0x66, 0x6f, 0x6f}; + Aws::Utils::ByteBuffer buf(raw, 3); + s.WriteBlob(member, buf); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("Zm9v"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, NullValueOmitted) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("item", ShapeType::String); + s.BeginStructure(root); + s.WriteNull(member); + s.EndStructure(); + // Null values are omitted in XML + EXPECT_EQ(s.GetPayload(), ""); +} + +TEST_F(XmlShapeSerializerTest, MultipleScalars) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema m1("a", ShapeType::Boolean); + Schema m2("b", ShapeType::Integer); + Schema m3("c", ShapeType::String); + s.BeginStructure(root); + s.WriteBoolean(m1, true); + s.WriteInteger(m2, 7); + s.WriteString(m3, "x"); + s.EndStructure(); + auto payload = s.GetPayload(); + EXPECT_NE(payload.find("true"), Aws::String::npos); + EXPECT_NE(payload.find("7"), Aws::String::npos); + EXPECT_NE(payload.find("x"), Aws::String::npos); +} + +// --- Nested structures --- + +TEST_F(XmlShapeSerializerTest, NestedStructure) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema nested("metadata", ShapeType::Structure); + Schema inner("key", ShapeType::String); + s.BeginStructure(root); + s.BeginNestedStructure(nested); + s.WriteString(inner, "val"); + s.EndNestedStructure(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("val"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, DeeplyNestedStructure) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema level1("l1", ShapeType::Structure); + Schema level2("l2", ShapeType::Structure); + Schema leaf("val", ShapeType::Integer); + s.BeginStructure(root); + s.BeginNestedStructure(level1); + s.BeginNestedStructure(level2); + s.WriteInteger(leaf, 99); + s.EndNestedStructure(); + s.EndNestedStructure(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("99"), Aws::String::npos); +} + +// --- Lists --- + +TEST_F(XmlShapeSerializerTest, ListOfStrings) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("tags", ShapeType::List); + Schema elem("member", ShapeType::String); + s.BeginStructure(root); + s.BeginList(listMember, 3); + s.WriteString(elem, "a"); + s.WriteString(elem, "b"); + s.WriteString(elem, "c"); + s.EndList(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("abc"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, ListOfIntegers) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("nums", ShapeType::List); + Schema elem("member", ShapeType::Integer); + s.BeginStructure(root); + s.BeginList(listMember, 3); + s.WriteInteger(elem, 1); + s.WriteInteger(elem, 2); + s.WriteInteger(elem, 3); + s.EndList(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("123"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, EmptyList) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("items", ShapeType::List); + s.BeginStructure(root); + s.BeginList(listMember, 0); + s.EndList(); + s.EndStructure(); + // Empty list still produces the wrapper element + EXPECT_NE(s.GetPayload().find(""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, ListOfStructures) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("items", ShapeType::List); + Schema structElem("member", ShapeType::Structure); + Schema field("id", ShapeType::Integer); + s.BeginStructure(root); + s.BeginList(listMember, 2); + s.BeginNestedStructure(structElem); + s.WriteInteger(field, 1); + s.EndNestedStructure(); + s.BeginNestedStructure(structElem); + s.WriteInteger(field, 2); + s.EndNestedStructure(); + s.EndList(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("12"), Aws::String::npos); +} + +// --- Maps --- + +TEST_F(XmlShapeSerializerTest, MapOfStrings) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema mapMember("headers", ShapeType::Map); + Schema valSchema("value", ShapeType::String); + s.BeginStructure(root); + s.BeginMap(mapMember, 2); + s.WriteMapKey("foo"); + s.WriteString(valSchema, "bar"); + s.WriteMapKey("baz"); + s.WriteString(valSchema, "qux"); + s.EndMap(); + s.EndStructure(); + auto payload = s.GetPayload(); + EXPECT_NE(payload.find("foobar"), Aws::String::npos); + EXPECT_NE(payload.find("bazqux"), Aws::String::npos); + EXPECT_NE(payload.find(""), Aws::String::npos); + EXPECT_NE(payload.find(""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, EmptyMap) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema mapMember("tags", ShapeType::Map); + s.BeginStructure(root); + s.BeginMap(mapMember, 0); + s.EndMap(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find(""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, MapOfStructures) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema mapMember("nodes", ShapeType::Map); + Schema valSchema("value", ShapeType::Structure); + Schema field("val", ShapeType::Integer); + s.BeginStructure(root); + s.BeginMap(mapMember, 1); + s.WriteMapKey("a"); + s.BeginNestedStructure(valSchema); + s.WriteInteger(field, 1); + s.EndNestedStructure(); + s.EndMap(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("a1"), Aws::String::npos); +} + +// --- Combinations --- + +TEST_F(XmlShapeSerializerTest, StructureWithListAndMap) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema strMember("name", ShapeType::String); + Schema listMember("tags", ShapeType::List); + Schema listElem("member", ShapeType::String); + Schema mapMember("meta", ShapeType::Map); + Schema mapVal("value", ShapeType::String); + + s.BeginStructure(root); + s.WriteString(strMember, "test"); + s.BeginList(listMember, 2); + s.WriteString(listElem, "t1"); + s.WriteString(listElem, "t2"); + s.EndList(); + s.BeginMap(mapMember, 1); + s.WriteMapKey("k"); + s.WriteString(mapVal, "v"); + s.EndMap(); + s.EndStructure(); + + auto payload = s.GetPayload(); + EXPECT_NE(payload.find("test"), Aws::String::npos); + EXPECT_NE(payload.find("t1t2"), Aws::String::npos); + EXPECT_NE(payload.find("kv"), Aws::String::npos); +} + +// --- XML Escaping --- + +TEST_F(XmlShapeSerializerTest, EscapesAmpersand) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("msg", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, "a&b"); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("a&b"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, EscapesLessThan) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("msg", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, "aa<b"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, EscapesGreaterThan) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("msg", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, "a>b"); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("a>b"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, EscapesQuotes) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("msg", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, "say \"hello\""); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("say "hello""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, EscapesMultipleSpecialChars) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("expr", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, "x < 5 & y > 3"); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("x < 5 & y > 3"), Aws::String::npos); +} + +// --- Depth limit --- + +TEST_F(XmlShapeSerializerTest, MaxDepthEnforcement) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema nested("n", ShapeType::Structure); + s.BeginStructure(root); + bool hitLimit = false; + for (int i = 0; i < 1000; i++) { + if (!s.BeginNestedStructure(nested)) { + hitLimit = true; + break; + } + } + EXPECT_TRUE(hitLimit); +} + +// --- xmlName trait --- + +TEST_F(XmlShapeSerializerTest, XmlNameOverridesMemberName) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema member("internalName", ShapeType::String); + member.WithXmlName("ExternalName"); + s.BeginStructure(root); + s.WriteString(member, "hello"); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("hello"), Aws::String::npos); + EXPECT_EQ(s.GetPayload().find(""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, XmlNameOnStructure) { + XmlShapeSerializer s; + Schema root("MyStruct", ShapeType::Structure); + root.WithXmlName("CustomRoot"); + Schema field("val", ShapeType::Integer); + s.BeginStructure(root); + s.WriteInteger(field, 42); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("42"), Aws::String::npos); +} + +// --- Flattened lists --- + +TEST_F(XmlShapeSerializerTest, FlattenedListOfStrings) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("item", ShapeType::List); + listMember.WithFlattened(true); + Schema elem("member", ShapeType::String); + s.BeginStructure(root); + s.BeginList(listMember, 3); + s.WriteString(elem, "a"); + s.WriteString(elem, "b"); + s.WriteString(elem, "c"); + s.EndList(); + s.EndStructure(); + auto payload = s.GetPayload(); + // Flattened: no wrapper, items repeat with list's xmlName + EXPECT_NE(payload.find("abc"), Aws::String::npos); + // Should NOT have a wrapper element around the items + EXPECT_EQ(payload.find(""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, FlattenedListWithXmlName) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("items", ShapeType::List); + listMember.WithFlattened(true).WithXmlName("Tag"); + Schema elem("member", ShapeType::String); + s.BeginStructure(root); + s.BeginList(listMember, 2); + s.WriteString(elem, "x"); + s.WriteString(elem, "y"); + s.EndList(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("xy"), Aws::String::npos); +} + +// --- Custom list item name --- + +TEST_F(XmlShapeSerializerTest, CustomListItemName) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("things", ShapeType::List); + listMember.WithListItemName("item"); + Schema elem("member", ShapeType::String); + s.BeginStructure(root); + s.BeginList(listMember, 2); + s.WriteString(elem, "a"); + s.WriteString(elem, "b"); + s.EndList(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("ab"), Aws::String::npos); +} + +// --- Custom map entry/key/value names --- + +TEST_F(XmlShapeSerializerTest, CustomMapNames) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema mapMember("tags", ShapeType::Map); + mapMember.WithMapEntryName("item").WithMapKeyName("tagKey").WithMapValueName("tagValue"); + Schema valSchema("value", ShapeType::String); + s.BeginStructure(root); + s.BeginMap(mapMember, 1); + s.WriteMapKey("color"); + s.WriteString(valSchema, "red"); + s.EndMap(); + s.EndStructure(); + EXPECT_NE(s.GetPayload().find("colorred"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, FlattenedMap) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema mapMember("tag", ShapeType::Map); + mapMember.WithFlattened(true); + Schema valSchema("value", ShapeType::String); + s.BeginStructure(root); + s.BeginMap(mapMember, 2); + s.WriteMapKey("k1"); + s.WriteString(valSchema, "v1"); + s.WriteMapKey("k2"); + s.WriteString(valSchema, "v2"); + s.EndMap(); + s.EndStructure(); + auto payload = s.GetPayload(); + // Flattened map: no wrapper, entry uses the map's xmlName + EXPECT_NE(payload.find("k1v1"), Aws::String::npos); + EXPECT_NE(payload.find("k2v2"), Aws::String::npos); +}