diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/JsonTraits.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/JsonTraits.h new file mode 100644 index 00000000000..8f8a3b0fa6c --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/JsonTraits.h @@ -0,0 +1,25 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#pragma once + +#include +#include +#include + +namespace smithy { +namespace schema { + +class JsonNameTrait : public Trait { + public: + explicit JsonNameTrait(const Aws::String& value) : m_value(value) {} + const Aws::String& GetValue() const { return m_value; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Aws::String m_value; +}; + +} // namespace schema +} // namespace smithy 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..732af32dd3a 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,18 @@ class Schema { uint16_t GetMemberCount() const { return m_memberCount; } + template + const T* GetTrait(const TraitKey& key) const { + return m_traits.Get(key); + } + + bool HasTrait(const TraitKeyBase& key) const { return m_traits.Has(key); } + + Schema& SetTrait(const TraitKeyBase& key, const Trait* trait) { + m_traits.Set(key, trait); + return *this; + } + private: const char* m_id = nullptr; ShapeType m_type = ShapeType::Structure; @@ -55,6 +68,7 @@ class Schema { int m_memberIndex = 0; const Schema* m_members = nullptr; uint16_t m_memberCount = 0; + TraitMap m_traits; }; } // namespace schema diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/SerdeTraits.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/SerdeTraits.h new file mode 100644 index 00000000000..039d7c195a1 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/SerdeTraits.h @@ -0,0 +1,26 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#pragma once + +#include +#include + +namespace smithy { +namespace schema { + +class TimestampFormatTrait : public Trait { + public: + enum class Format { DATE_TIME, HTTP_DATE, EPOCH_SECONDS }; + + explicit TimestampFormatTrait(Format format) : m_format(format) {} + Format GetFormat() const { return m_format; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Format m_format; +}; + +} // namespace schema +} // namespace smithy diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/Trait.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/Trait.h new file mode 100644 index 00000000000..266de19ebd4 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/Trait.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace smithy { +namespace schema { +class SMITHY_API Trait { + public: + virtual ~Trait() = default; +}; +} // namespace schema +} // namespace smithy \ No newline at end of file diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/TraitKey.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/TraitKey.h new file mode 100644 index 00000000000..73b4312501b --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/TraitKey.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +namespace smithy { +namespace schema { +class SMITHY_API TraitKeyBase { + public: + int GetId() const { return m_id; } + + protected: + explicit TraitKeyBase(int id) : m_id(id) {} + + private: + int m_id; +}; + +SMITHY_API int NextTraitKey(); + +template +class TraitKey : public TraitKeyBase { + public: + static const TraitKey& Instance() { + static const TraitKey instance(NextTraitKey()); + return instance; + } + + private: + explicit TraitKey(int id) : TraitKeyBase(id) {} +}; +} // namespace schema +} // namespace smithy \ No newline at end of file diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/TraitMap.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/TraitMap.h new file mode 100644 index 00000000000..469a5c1dee4 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/TraitMap.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +namespace smithy { +namespace schema { +class SMITHY_API TraitMap { + public: + TraitMap() = default; + + template + const T* Get(const TraitKey& key) const { + int idx = key.GetId(); + if (idx >= static_cast(m_value.size())) return nullptr; + return static_cast(m_value[idx]); + } + + bool Has(const TraitKeyBase& key) const { + int idx = key.GetId(); + if (idx >= static_cast(m_value.size())) return false; + return m_value[idx] != nullptr; + } + + void Set(const TraitKeyBase& key, const Trait* trait) { + int idx = key.GetId(); + if (idx >= static_cast(m_value.size())) { + m_value.resize(idx + 1, nullptr); + } + m_value[idx] = trait; + } + + private: + Aws::Vector m_value; +}; +} // namespace schema +} // namespace smithy \ No newline at end of file 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..f39db5978c0 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/XmlTraits.h @@ -0,0 +1,87 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#pragma once + +#include +#include +#include + +namespace smithy { +namespace schema { + +class XmlNameTrait : public Trait { + public: + explicit XmlNameTrait(const Aws::String& value) : m_value(value) {} + const Aws::String& GetValue() const { return m_value; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Aws::String m_value; +}; + +class XmlFlattenedTrait : public Trait { + public: + static const TraitKey& KEY() { return TraitKey::Instance(); } +}; + +class XmlAttributeTrait : public Trait { + public: + static const TraitKey& KEY() { return TraitKey::Instance(); } +}; + +class XmlNamespaceTrait : public Trait { + public: + XmlNamespaceTrait(const Aws::String& uri, const Aws::String& prefix = "") : m_uri(uri), m_prefix(prefix) {} + const Aws::String& GetUri() const { return m_uri; } + const Aws::String& GetPrefix() const { return m_prefix; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Aws::String m_uri; + Aws::String m_prefix; +}; + +class XmlListItemNameTrait : public Trait { + public: + explicit XmlListItemNameTrait(const Aws::String& value) : m_value(value) {} + const Aws::String& GetValue() const { return m_value; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Aws::String m_value; +}; + +class XmlMapEntryNameTrait : public Trait { + public: + explicit XmlMapEntryNameTrait(const Aws::String& value) : m_value(value) {} + const Aws::String& GetValue() const { return m_value; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Aws::String m_value; +}; + +class XmlMapKeyNameTrait : public Trait { + public: + explicit XmlMapKeyNameTrait(const Aws::String& value) : m_value(value) {} + const Aws::String& GetValue() const { return m_value; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Aws::String m_value; +}; + +class XmlMapValueNameTrait : public Trait { + public: + explicit XmlMapValueNameTrait(const Aws::String& value) : m_value(value) {} + const Aws::String& GetValue() const { return m_value; } + static const TraitKey& KEY() { return TraitKey::Instance(); } + + private: + Aws::String m_value; +}; + +} // namespace schema +} // namespace smithy diff --git a/src/aws-cpp-sdk-core/source/smithy/client/schema/JsonShapeSerializer.cpp b/src/aws-cpp-sdk-core/source/smithy/client/schema/JsonShapeSerializer.cpp index 115304c125e..299d873983a 100644 --- a/src/aws-cpp-sdk-core/source/smithy/client/schema/JsonShapeSerializer.cpp +++ b/src/aws-cpp-sdk-core/source/smithy/client/schema/JsonShapeSerializer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -193,7 +194,8 @@ class JsonShapeSerializer::Impl { if (m_depth > 0 && m_isMap[m_depth]) { WriteKey(m_currentMapKey); } else { - WriteKey(schema.GetMemberName()); + const auto* jsonName = schema.GetTrait(JsonNameTrait::KEY()); + WriteKey(jsonName ? jsonName->GetValue() : schema.GetMemberName()); } } }; diff --git a/src/aws-cpp-sdk-core/source/smithy/client/schema/TraitKey.cpp b/src/aws-cpp-sdk-core/source/smithy/client/schema/TraitKey.cpp new file mode 100644 index 00000000000..ce7ff95d2e8 --- /dev/null +++ b/src/aws-cpp-sdk-core/source/smithy/client/schema/TraitKey.cpp @@ -0,0 +1,10 @@ +#include + +namespace smithy { +namespace schema { + +static int s_traitIdCounter = 0; + +int NextTraitKey() { return s_traitIdCounter++; } +} // namespace schema +} // namespace smithy \ No newline at end of file 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..fc220915929 --- /dev/null +++ b/src/aws-cpp-sdk-core/source/smithy/client/schema/XmlShapeSerializer.cpp @@ -0,0 +1,409 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#include +#include +#include +#include + +using namespace smithy::schema; +using namespace Aws::Utils; + +static constexpr int MAX_DEPTH = 500; + +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* ns = schema.GetTrait(XmlNamespaceTrait::KEY()); + if (ns && !ns->GetUri().empty()) { + if (ns->GetPrefix().empty()) { + m_buf += " xmlns=\""; + } else { + m_buf += " xmlns:"; + m_buf += ns->GetPrefix(); + m_buf += "=\""; + } + m_buf += ns->GetUri(); + 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); + } + + static 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++; + } + } + } + + static Aws::String GetXmlName(const Schema& schema) { + const auto* trait = schema.GetTrait(XmlNameTrait::KEY()); + if (trait) { + return trait->GetValue(); + } + return schema.GetMemberName(); + } + + static bool IsFlattened(const Schema& schema) { return schema.HasTrait(XmlFlattenedTrait::KEY()); } + + static Aws::String GetListItemName(const Schema& schema) { + const auto* trait = schema.GetTrait(XmlListItemNameTrait::KEY()); + if (trait) { + return trait->GetValue(); + } + return "member"; + } + + static Aws::String GetMapEntryName(const Schema& schema) { + const auto* trait = schema.GetTrait(XmlMapEntryNameTrait::KEY()); + if (trait) { + return trait->GetValue(); + } + return "entry"; + } + + static Aws::String GetMapKeyName(const Schema& schema) { + const auto* trait = schema.GetTrait(XmlMapKeyNameTrait::KEY()); + if (trait) { + return trait->GetValue(); + } + return "key"; + } + + static Aws::String GetMapValueName(const Schema& schema) { + const auto* trait = schema.GetTrait(XmlMapValueNameTrait::KEY()); + if (trait) { + return trait->GetValue(); + } + return "value"; + } +}; + +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/JsonShapeSerializerTest.cpp b/tests/aws-cpp-sdk-core-tests/smithy/client/schema/JsonShapeSerializerTest.cpp index b794ff40afa..585c0648c21 100644 --- a/tests/aws-cpp-sdk-core-tests/smithy/client/schema/JsonShapeSerializerTest.cpp +++ b/tests/aws-cpp-sdk-core-tests/smithy/client/schema/JsonShapeSerializerTest.cpp @@ -5,6 +5,7 @@ #include #include #include +#include using namespace smithy::schema; @@ -439,3 +440,29 @@ TEST_F(JsonShapeSerializerTest, MaxDepthEnforcement) { ASSERT_FALSE(outcome.IsSuccess()); EXPECT_NE(outcome.GetError().GetMessage().find("depth"), Aws::String::npos); } + +// --- JsonNameTrait --- + +TEST_F(JsonShapeSerializerTest, JsonNameOverridesMemberName) { + JsonShapeSerializer s; + Schema root; + Schema member("internalName", ShapeType::String); + static const JsonNameTrait s_jsonName("ExternalName"); + member.SetTrait(JsonNameTrait::KEY(), &s_jsonName); + s.BeginStructure(root); + s.WriteString(member, "hello"); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + EXPECT_NE(payload.find("\"ExternalName\":\"hello\""), Aws::String::npos); + EXPECT_EQ(payload.find("\"internalName\""), Aws::String::npos); +} + +TEST_F(JsonShapeSerializerTest, NoJsonNameUsesGetMemberName) { + JsonShapeSerializer s; + Schema root; + Schema member("fieldName", ShapeType::String); + s.BeginStructure(root); + s.WriteString(member, "value"); + s.EndStructure(); + EXPECT_NE(s.GetPayload().GetResult().find("\"fieldName\":\"value\""), Aws::String::npos); +} 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..9044acf9ceb --- /dev/null +++ b/tests/aws-cpp-sdk-core-tests/smithy/client/schema/XmlShapeSerializerTest.cpp @@ -0,0 +1,515 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#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().GetResult(), ""); +} + +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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult(), ""); +} + +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().GetResult(); + 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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult().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().GetResult(); + 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().GetResult().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().GetResult().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().GetResult(); + 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().GetResult().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().GetResult().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().GetResult().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().GetResult().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); + static const XmlNameTrait s_extName("ExternalName"); + member.SetTrait(XmlNameTrait::KEY(), &s_extName); + s.BeginStructure(root); + s.WriteString(member, "hello"); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + EXPECT_NE(payload.find("hello"), Aws::String::npos); + EXPECT_EQ(payload.find(""), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, XmlNameOnStructure) { + XmlShapeSerializer s; + Schema root("MyStruct", ShapeType::Structure); + static const XmlNameTrait s_customRoot("CustomRoot"); + root.SetTrait(XmlNameTrait::KEY(), &s_customRoot); + Schema field("val", ShapeType::Integer); + s.BeginStructure(root); + s.WriteInteger(field, 42); + s.EndStructure(); + EXPECT_NE(s.GetPayload().GetResult().find("42"), Aws::String::npos); +} + +// --- Flattened lists --- + +TEST_F(XmlShapeSerializerTest, FlattenedListOfStrings) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema listMember("item", ShapeType::List); + static const XmlFlattenedTrait s_flat1; + listMember.SetTrait(XmlFlattenedTrait::KEY(), &s_flat1); + 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().GetResult(); + // 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); + static const XmlFlattenedTrait s_flat2; + static const XmlNameTrait s_tag("Tag"); + listMember.SetTrait(XmlFlattenedTrait::KEY(), &s_flat2); + listMember.SetTrait(XmlNameTrait::KEY(), &s_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().GetResult().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); + static const XmlListItemNameTrait s_itemName("item"); + listMember.SetTrait(XmlListItemNameTrait::KEY(), &s_itemName); + 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().GetResult().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); + static const XmlMapEntryNameTrait s_entryName("item"); + static const XmlMapKeyNameTrait s_keyName("tagKey"); + static const XmlMapValueNameTrait s_valueName("tagValue"); + mapMember.SetTrait(XmlMapEntryNameTrait::KEY(), &s_entryName); + mapMember.SetTrait(XmlMapKeyNameTrait::KEY(), &s_keyName); + mapMember.SetTrait(XmlMapValueNameTrait::KEY(), &s_valueName); + 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().GetResult().find("colorred"), Aws::String::npos); +} + +TEST_F(XmlShapeSerializerTest, FlattenedMap) { + XmlShapeSerializer s; + Schema root("Root", ShapeType::Structure); + Schema mapMember("tag", ShapeType::Map); + static const XmlFlattenedTrait s_flat3; + mapMember.SetTrait(XmlFlattenedTrait::KEY(), &s_flat3); + 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().GetResult(); + // 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); +}